Skip to content

Commit

Permalink
Merge pull request #557 from jvalue/543-feature-enable-transforming-o…
Browse files Browse the repository at this point in the history
…f-value-types

feat(operators): Parse text into other Valuetypes with `asText`, `asDecimal`, `asBoolean`, `asInteger`
  • Loading branch information
TungstnBallon authored May 17, 2024
2 parents 8c770d5 + fec463e commit ed3eaca
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import { strict as assert } from 'assert';

import {
type AtomicValueType,
type BooleanValuetype,
type DecimalValuetype,
type IntegerValuetype,
type InternalValueRepresentation,
type TextValuetype,
type ValueType,
ValueTypeVisitor,
} from '@jvalue/jayvee-language-server';

const NUMBER_REGEX = /^[+-]?([0-9]*[,.])?[0-9]+([eE][+-]?\d+)?$/;

const TRUE_REGEX = /^true$/i;
const FALSE_REGEX = /^false$/i;

export function parseValueToInternalRepresentation<
I extends InternalValueRepresentation,
>(value: string, valueType: ValueType<I>): I | undefined {
Expand All @@ -35,46 +34,20 @@ class InternalRepresentationParserVisitor extends ValueTypeVisitor<
super();
}

visitBoolean(): boolean | undefined {
if (TRUE_REGEX.test(this.value)) {
return true;
} else if (FALSE_REGEX.test(this.value)) {
return false;
}
return undefined;
visitBoolean(vt: BooleanValuetype): boolean | undefined {
return vt.fromString(this.value);
}

visitDecimal(): number | undefined {
if (!NUMBER_REGEX.test(this.value)) {
return undefined;
}

return Number.parseFloat(this.value.replace(',', '.'));
visitDecimal(vt: DecimalValuetype): number | undefined {
return vt.fromString(this.value);
}

visitInteger(): number | undefined {
/**
* Reuse decimal number parsing to capture valid scientific notation
* of integers like 5.3e3 = 5300. In contrast to decimal, if the final number
* is not a valid integer, returns undefined.
*/
const decimalNumber = this.visitDecimal();

if (decimalNumber === undefined) {
return undefined;
}

const integerNumber = Math.trunc(decimalNumber);

if (decimalNumber !== integerNumber) {
return undefined;
}

return integerNumber;
visitInteger(vt: IntegerValuetype): number | undefined {
return vt.fromString(this.value);
}

visitText(): string {
return this.value;
visitText(vt: TextValuetype): string {
return vt.fromString(this.value);
}

visitAtomicValueType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

pipeline TestPipeline {
transform ToBool {
from asInteger oftype integer;
to asBool oftype boolean;
from integer oftype integer;
to boolean oftype boolean;

asBool: asInteger != 0;
boolean: integer != 0;
}

block TestExtractor oftype TestTableExtractor {
Expand Down
3 changes: 1 addition & 2 deletions libs/language-server/src/grammar/expression.langium
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Expression:
ReplaceExpression;

// The nesting of the following rules implies the precedence of the operators:

ReplaceExpression infers Expression:
OrExpression ({infer TernaryExpression.first=current} operator='replace' second=OrExpression 'with' third=OrExpression)*;

Expand Down Expand Up @@ -53,7 +52,7 @@ PrimaryExpression infers Expression:
| ExpressionLiteral;

UnaryExpression:
operator=('not' | '+' | '-' | 'sqrt' | 'floor' | 'ceil' | 'round' | 'lowercase' | 'uppercase') expression=PrimaryExpression;
operator=('not' | '+' | '-' | 'sqrt' | 'floor' | 'ceil' | 'round' | 'lowercase' | 'uppercase' | 'asDecimal' | 'asInteger' | 'asBoolean' | 'asText') expression=PrimaryExpression;

ExpressionLiteral:
ValueLiteral | FreeVariableLiteral;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only
import { type InternalValueRepresentation } from '..';
import { executeDefaultTextToTextExpression } from '../test-utils';

async function expectSuccess<I extends InternalValueRepresentation>(
op: string,
input: string,
expected: I,
) {
let result: InternalValueRepresentation | undefined = undefined;
try {
result = await executeDefaultTextToTextExpression(
`${op} inputValue`,
input,
);
} finally {
expect(result).toEqual(expected);
}
}

async function expectError(op: string, input: string) {
let result: InternalValueRepresentation | undefined = undefined;
try {
result = await executeDefaultTextToTextExpression(
`${op} inputValue`,
input,
);
} catch {
result = undefined;
} finally {
expect(result).toBeUndefined();
}
}

describe('The asText operator', () => {
it('should parse text successfully', async () => {
await expectSuccess('asText', 'someText', 'someText');
});
});

describe('The asDecimal operator', () => {
it('should parse positive decimals successfully', async () => {
await expectSuccess('asDecimal', '1.6', 1.6);
});

it('should parse decimals with commas successfully', async () => {
await expectSuccess('asDecimal', '1,6', 1.6);
});

it('should parse negative decimals with commas successfully', async () => {
await expectSuccess('asDecimal', '-1,6', -1.6);
});
});

describe('The asInteger operator', () => {
it('should parse positive integers successfully', async () => {
await expectSuccess('asInteger', '32', 32);
});

it('should parse negative integers successfully', async () => {
await expectSuccess('asInteger', '-1', -1);
});

it('should fail with decimal values', async () => {
await expectError('asInteger', '32.5');
});
});

describe('The asBoolean operator', () => {
it('should parse true and True successfully', async () => {
await expectSuccess('asBoolean', 'true', true);
await expectSuccess('asBoolean', 'True', true);
});

it('should parse false and False successfully', async () => {
await expectSuccess('asBoolean', 'false', false);
await expectSuccess('asBoolean', 'False', false);
});

it('should fail with 0 and 1', async () => {
await expectError('asBoolean', '0');
await expectError('asBoolean', '1');
});

it('should fail on a arbitrary string', async () => {
await expectError('asBoolean', 'notABoolean');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import { type ValueTypeProvider } from '../../wrappers';
import { DefaultUnaryOperatorEvaluator } from '../operator-evaluator';
import { STRING_TYPEGUARD } from '../typeguards';

export class AsTextOperatorEvaluator extends DefaultUnaryOperatorEvaluator<
string,
string
> {
constructor(private readonly valueTypeProvider: ValueTypeProvider) {
super('asText', STRING_TYPEGUARD);
}
override doEvaluate(operandValue: string): string {
return this.valueTypeProvider.Primitives.Text.fromString(operandValue);
}
}

export class AsDecimalOperatorEvaluator extends DefaultUnaryOperatorEvaluator<
string,
number
> {
constructor(private readonly valueTypeProvider: ValueTypeProvider) {
super('asDecimal', STRING_TYPEGUARD);
}
override doEvaluate(operandValue: string): number {
const dec =
this.valueTypeProvider.Primitives.Decimal.fromString(operandValue);
if (dec === undefined) {
throw new Error(`Could not parse "${operandValue}" into a Decimal`);
}
return dec;
}
}

export class AsIntegerOperatorEvaluator extends DefaultUnaryOperatorEvaluator<
string,
number
> {
constructor(private readonly valueTypeProvider: ValueTypeProvider) {
super('asInteger', STRING_TYPEGUARD);
}
override doEvaluate(operandValue: string): number {
const int =
this.valueTypeProvider.Primitives.Integer.fromString(operandValue);
if (int === undefined) {
throw new Error(`Could not parse "${operandValue}" into an Integer`);
}
return int;
}
}

export class AsBooleanOperatorEvaluator extends DefaultUnaryOperatorEvaluator<
string,
boolean
> {
constructor(private readonly valueTypeProvider: ValueTypeProvider) {
super('asBoolean', STRING_TYPEGUARD);
}
override doEvaluate(operandValue: string): boolean {
const bool =
this.valueTypeProvider.Primitives.Boolean.fromString(operandValue);
if (bool === undefined) {
throw new Error(`Could not parse "${operandValue}" into a Boolean`);
}
return bool;
}
}
22 changes: 22 additions & 0 deletions libs/language-server/src/lib/ast/expressions/operator-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ import { ModuloOperatorEvaluator } from './evaluators/modulo-operator-evaluator'
import { MultiplicationOperatorEvaluator } from './evaluators/multiplication-operator-evaluator';
import { NotOperatorEvaluator } from './evaluators/not-operator-evaluator';
import { OrOperatorEvaluator } from './evaluators/or-operator-evaluator';
import {
AsBooleanOperatorEvaluator,
AsDecimalOperatorEvaluator,
AsIntegerOperatorEvaluator,
AsTextOperatorEvaluator,
} from './evaluators/parse-operators-evaluators';
import { PlusOperatorEvaluator } from './evaluators/plus-operator-evaluator';
import { PowOperatorEvaluator } from './evaluators/pow-operator-evaluator';
import { ReplaceOperatorEvaluator } from './evaluators/replace-operator-evaluator';
Expand Down Expand Up @@ -60,6 +66,12 @@ import { IntegerConversionOperatorTypeComputer } from './type-computers/integer-
import { LogicalOperatorTypeComputer } from './type-computers/logical-operator-type-computer';
import { MatchesOperatorTypeComputer } from './type-computers/matches-operator-type-computer';
import { NotOperatorTypeComputer } from './type-computers/not-operator-type-computer';
import {
AsBooleanOperatorTypeComputer,
AsDecimalOperatorTypeComputer,
AsIntegerOperatorTypeComputer,
AsTextOperatorTypeComputer,
} from './type-computers/parse-opterators-type-computer';
import { RelationalOperatorTypeComputer } from './type-computers/relational-operator-type-computer';
import { ReplaceOperatorTypeComputer } from './type-computers/replace-operator-type-computer';
import { SignOperatorTypeComputer } from './type-computers/sign-operator-type-computer';
Expand Down Expand Up @@ -94,6 +106,10 @@ export class DefaultOperatorEvaluatorRegistry
round: new RoundOperatorEvaluator(),
lowercase: new LowercaseOperatorEvaluator(),
uppercase: new UppercaseOperatorEvaluator(),
asDecimal: new AsDecimalOperatorEvaluator(this.valueTypeProvider),
asInteger: new AsIntegerOperatorEvaluator(this.valueTypeProvider),
asBoolean: new AsBooleanOperatorEvaluator(this.valueTypeProvider),
asText: new AsTextOperatorEvaluator(this.valueTypeProvider),
};
binary = {
pow: new PowOperatorEvaluator(),
Expand All @@ -118,6 +134,8 @@ export class DefaultOperatorEvaluatorRegistry
ternary = {
replace: new ReplaceOperatorEvaluator(),
};

constructor(private readonly valueTypeProvider: ValueTypeProvider) {}
}

export class DefaultOperatorTypeComputerRegistry
Expand All @@ -133,6 +151,10 @@ export class DefaultOperatorTypeComputerRegistry
round: new IntegerConversionOperatorTypeComputer(this.valueTypeProvider),
lowercase: new StringTransformTypeComputer(this.valueTypeProvider),
uppercase: new StringTransformTypeComputer(this.valueTypeProvider),
asDecimal: new AsDecimalOperatorTypeComputer(this.valueTypeProvider),
asInteger: new AsIntegerOperatorTypeComputer(this.valueTypeProvider),
asBoolean: new AsBooleanOperatorTypeComputer(this.valueTypeProvider),
asText: new AsTextOperatorTypeComputer(this.valueTypeProvider),
};
binary = {
pow: new ExponentialOperatorTypeComputer(this.valueTypeProvider),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import {
type ValueType,
type ValueTypeProvider,
} from '../../wrappers/value-type';
import { DefaultUnaryOperatorTypeComputer } from '../operator-type-computer';

export class AsDecimalOperatorTypeComputer extends DefaultUnaryOperatorTypeComputer {
constructor(protected readonly valueTypeProvider: ValueTypeProvider) {
super(valueTypeProvider.Primitives.Text);
}

override doComputeType(): ValueType {
return this.valueTypeProvider.Primitives.Decimal;
}
}

export class AsTextOperatorTypeComputer extends DefaultUnaryOperatorTypeComputer {
constructor(protected readonly valueTypeProvider: ValueTypeProvider) {
super(valueTypeProvider.Primitives.Text);
}

override doComputeType(): ValueType {
return this.valueTypeProvider.Primitives.Text;
}
}

export class AsIntegerOperatorTypeComputer extends DefaultUnaryOperatorTypeComputer {
constructor(protected readonly valueTypeProvider: ValueTypeProvider) {
super(valueTypeProvider.Primitives.Text);
}

override doComputeType(): ValueType {
return this.valueTypeProvider.Primitives.Integer;
}
}

export class AsBooleanOperatorTypeComputer extends DefaultUnaryOperatorTypeComputer {
constructor(protected readonly valueTypeProvider: ValueTypeProvider) {
super(valueTypeProvider.Primitives.Text);
}

override doComputeType(): ValueType {
return this.valueTypeProvider.Primitives.Boolean;
}
}
Loading

0 comments on commit ed3eaca

Please sign in to comment.