diff --git a/libs/execution/src/lib/types/value-types/internal-representation-parsing.ts b/libs/execution/src/lib/types/value-types/internal-representation-parsing.ts index 860bf488d..39c2b0323 100644 --- a/libs/execution/src/lib/types/value-types/internal-representation-parsing.ts +++ b/libs/execution/src/lib/types/value-types/internal-representation-parsing.ts @@ -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 | undefined { @@ -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( diff --git a/libs/extensions/tabular/exec/test/assets/table-transformer-executor/valid-column-type-change.jv b/libs/extensions/tabular/exec/test/assets/table-transformer-executor/valid-column-type-change.jv index a85a6b2ad..d17c866ba 100644 --- a/libs/extensions/tabular/exec/test/assets/table-transformer-executor/valid-column-type-change.jv +++ b/libs/extensions/tabular/exec/test/assets/table-transformer-executor/valid-column-type-change.jv @@ -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 { diff --git a/libs/language-server/src/grammar/expression.langium b/libs/language-server/src/grammar/expression.langium index 7c63b1266..d58730685 100644 --- a/libs/language-server/src/grammar/expression.langium +++ b/libs/language-server/src/grammar/expression.langium @@ -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)*; @@ -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; diff --git a/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.spec.ts b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.spec.ts new file mode 100644 index 000000000..5f6e56de2 --- /dev/null +++ b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.spec.ts @@ -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( + 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'); + }); +}); diff --git a/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts new file mode 100644 index 000000000..920b243e1 --- /dev/null +++ b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts @@ -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; + } +} diff --git a/libs/language-server/src/lib/ast/expressions/operator-registry.ts b/libs/language-server/src/lib/ast/expressions/operator-registry.ts index fc0fc9b50..5179ef689 100644 --- a/libs/language-server/src/lib/ast/expressions/operator-registry.ts +++ b/libs/language-server/src/lib/ast/expressions/operator-registry.ts @@ -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'; @@ -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'; @@ -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(), @@ -118,6 +134,8 @@ export class DefaultOperatorEvaluatorRegistry ternary = { replace: new ReplaceOperatorEvaluator(), }; + + constructor(private readonly valueTypeProvider: ValueTypeProvider) {} } export class DefaultOperatorTypeComputerRegistry @@ -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), diff --git a/libs/language-server/src/lib/ast/expressions/type-computers/parse-opterators-type-computer.ts b/libs/language-server/src/lib/ast/expressions/type-computers/parse-opterators-type-computer.ts new file mode 100644 index 000000000..7cac652a7 --- /dev/null +++ b/libs/language-server/src/lib/ast/expressions/type-computers/parse-opterators-type-computer.ts @@ -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; + } +} diff --git a/libs/language-server/src/lib/ast/expressions/typeguards.ts b/libs/language-server/src/lib/ast/expressions/typeguards.ts index 40c8c559c..a79e270c2 100644 --- a/libs/language-server/src/lib/ast/expressions/typeguards.ts +++ b/libs/language-server/src/lib/ast/expressions/typeguards.ts @@ -2,6 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0-only +import { + type ValuetypeAssignment, + isValuetypeAssignment, +} from '../generated/ast'; + import { type InternalValueRepresentation, type InternalValueRepresentationTypeguard, @@ -36,6 +41,12 @@ export const REGEXP_TYPEGUARD: InternalValueRepresentationTypeguard = ( return value instanceof RegExp; }; +export const VALUETYPEASSIGNMENT_TYPEGUARD: InternalValueRepresentationTypeguard< + ValuetypeAssignment +> = (value: InternalValueRepresentation): value is ValuetypeAssignment => { + return isValuetypeAssignment(value); +}; + export function isEveryValueDefined(array: (T | undefined)[]): array is T[] { return array.every((value) => value !== undefined); } diff --git a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/boolean-value-type.ts b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/boolean-value-type.ts index 063c280f2..def1e2c64 100644 --- a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/boolean-value-type.ts +++ b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/boolean-value-type.ts @@ -7,6 +7,9 @@ import { type ValueTypeVisitor } from '../value-type'; import { PrimitiveValueType } from './primitive-value-type'; +const TRUE_REGEX = /^true$/i; +const FALSE_REGEX = /^false$/i; + export class BooleanValuetype extends PrimitiveValueType { acceptVisitor(visitor: ValueTypeVisitor): R { return visitor.visitBoolean(this); @@ -36,4 +39,13 @@ A boolean value. Examples: true, false `.trim(); } + + override fromString(s: string): boolean | undefined { + if (TRUE_REGEX.test(s)) { + return true; + } else if (FALSE_REGEX.test(s)) { + return false; + } + return undefined; + } } diff --git a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/decimal-value-type.ts b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/decimal-value-type.ts index c79bc14f0..665fc7950 100644 --- a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/decimal-value-type.ts +++ b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/decimal-value-type.ts @@ -7,6 +7,16 @@ import { type ValueTypeVisitor } from '../value-type'; import { PrimitiveValueType } from './primitive-value-type'; +const NUMBER_REGEX = /^[+-]?([0-9]*[,.])?[0-9]+([eE][+-]?\d+)?$/; + +export function parseDecimal(s: string): number | undefined { + if (!NUMBER_REGEX.test(s)) { + return undefined; + } + + return Number.parseFloat(s.replace(',', '.')); +} + export class DecimalValuetype extends PrimitiveValueType { acceptVisitor(visitor: ValueTypeVisitor): R { return visitor.visitDecimal(this); @@ -36,4 +46,8 @@ A decimal value. Example: 3.14 `.trim(); } + + override fromString(s: string): number | undefined { + return parseDecimal(s); + } } diff --git a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/integer-value-type.ts b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/integer-value-type.ts index b4f9eecc6..8c1239f4e 100644 --- a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/integer-value-type.ts +++ b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/integer-value-type.ts @@ -5,7 +5,7 @@ import { type InternalValueRepresentation } from '../../../expressions/internal-value-representation'; import { type ValueType, type ValueTypeVisitor } from '../value-type'; -import { DecimalValuetype } from './decimal-value-type'; +import { DecimalValuetype, parseDecimal } from './decimal-value-type'; import { PrimitiveValueType } from './primitive-value-type'; export class IntegerValuetype extends PrimitiveValueType { @@ -41,4 +41,25 @@ An integer value. Example: 3 `.trim(); } + + override fromString(s: string): 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 = parseDecimal(s); + + if (decimalNumber === undefined) { + return undefined; + } + + const integerNumber = Math.trunc(decimalNumber); + + if (decimalNumber !== integerNumber) { + return undefined; + } + + return integerNumber; + } } diff --git a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/primitive-value-type.ts b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/primitive-value-type.ts index 654fd8b37..2d1f63913 100644 --- a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/primitive-value-type.ts +++ b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/primitive-value-type.ts @@ -33,6 +33,11 @@ export abstract class PrimitiveValueType< getUserDoc(): string | undefined { return undefined; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fromString(_s: string): I | undefined { + return undefined; + } } export function isPrimitiveValueType(v: unknown): v is PrimitiveValueType { diff --git a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/text-value-type.ts b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/text-value-type.ts index bc5fdf476..9bef893c0 100644 --- a/libs/language-server/src/lib/ast/wrappers/value-type/primitive/text-value-type.ts +++ b/libs/language-server/src/lib/ast/wrappers/value-type/primitive/text-value-type.ts @@ -36,4 +36,8 @@ A text value. Example: "Hello World" `.trim(); } + + override fromString(s: string): string { + return s; + } } diff --git a/libs/language-server/src/lib/jayvee-module.ts b/libs/language-server/src/lib/jayvee-module.ts index b2f082042..4a55a408d 100644 --- a/libs/language-server/src/lib/jayvee-module.ts +++ b/libs/language-server/src/lib/jayvee-module.ts @@ -88,7 +88,8 @@ export const JayveeModule: Module< services.ValueTypeProvider, services.WrapperFactories, ), - EvaluatorRegistry: () => new DefaultOperatorEvaluatorRegistry(), + EvaluatorRegistry: (services) => + new DefaultOperatorEvaluatorRegistry(services.ValueTypeProvider), }, ValueTypeProvider: () => new ValueTypeProvider(), WrapperFactories: (services) =>