From 7faf4fc3af309c672361ff8e0d576fbac96f91e5 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Sat, 27 Apr 2024 10:48:43 +0200 Subject: [PATCH 1/4] test(operators): Add tests for parse operators tests for asDecimal, asInteger, asBoolean and asText operators --- .../parse-operators-evaluators.spec.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.spec.ts 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..b8e315e8c --- /dev/null +++ b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.spec.ts @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only +import { executeDefaultTextToTextExpression } from '../test-utils'; + +describe('The asText operator', () => { + it('should parse text successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asText inputValue', + 'someText', + ); + + expect(result).toEqual('someText'); + }); +}); + +describe('The asDecimal operator', () => { + it('should parse positive decimals successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asDecimal inputValue', + '1.6', + ); + + expect(result).toEqual(1.6); + }); + + it('should parse decimals with commas successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asDecimal inputValue', + '1,6', + ); + + expect(result).toEqual(1.6); + }); + + it('should parse negative decimals with commas successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asDecimal inputValue', + '-1,6', + ); + + expect(result).toEqual(-1.6); + }); +}); + +describe('The asInteger operator', () => { + it('should parse positive integers successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asInteger inputValue', + '32', + ); + + expect(result).toEqual(32); + }); + + it('should parse negative integers successfully', async () => { + const result = await executeDefaultTextToTextExpression( + 'asInteger inputValue', + '-1', + ); + + expect(result).toEqual(-1); + }); + + it('should fail with decimal values', async () => { + const result = await executeDefaultTextToTextExpression( + 'asInteger inputValue', + '32.5', + ); + + expect(result).toEqual(undefined); + }); +}); + +describe('The asBoolean operator', () => { + it('should parse true and True successfully', async () => { + let result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + 'true', + ); + + expect(result).toEqual(true); + + result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + 'True', + ); + + expect(result).toEqual(true); + }); + + it('should parse false and False successfully', async () => { + let result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + 'false', + ); + + expect(result).toEqual(false); + + result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + 'False', + ); + + expect(result).toEqual(false); + }); + + it('should fail with 0 and 1', async () => { + let result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + '0', + ); + + expect(result).toEqual(undefined); + + result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + '1', + ); + + expect(result).toEqual(undefined); + }); + + it('should fail on a random string', async () => { + const result = await executeDefaultTextToTextExpression( + 'asBoolean inputValue', + 'notABoolean', + ); + + expect(result).toEqual(undefined); + }); +}); From d447cc1e3b485d6d2337b863d20fb8316c778eae Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Sat, 27 Apr 2024 10:51:04 +0200 Subject: [PATCH 2/4] feat(operators): Add parse operators adds asDecimal, asInteger, asBoolean and asText operators --- .../valid-column-type-change.jv | 6 +-- .../src/grammar/expression.langium | 2 +- .../evaluators/parse-operators-evaluators.ts | 54 +++++++++++++++++++ .../lib/ast/expressions/operator-registry.ts | 20 +++++++ .../parse-opterators-type-computer.ts | 49 +++++++++++++++++ 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts create mode 100644 libs/language-server/src/lib/ast/expressions/type-computers/parse-opterators-type-computer.ts 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..e8ed6f7e4 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 bool oftype boolean; - asBool: asInteger != 0; + bool: 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..17efe09ca 100644 --- a/libs/language-server/src/grammar/expression.langium +++ b/libs/language-server/src/grammar/expression.langium @@ -53,7 +53,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.ts b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts new file mode 100644 index 000000000..4cc5e8842 --- /dev/null +++ b/libs/language-server/src/lib/ast/expressions/evaluators/parse-operators-evaluators.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { DefaultUnaryOperatorEvaluator } from '../operator-evaluator'; +import { STRING_TYPEGUARD } from '../typeguards'; + +export class AsTextOperatorEvaluator extends DefaultUnaryOperatorEvaluator< + string, + string +> { + constructor() { + super('asText', STRING_TYPEGUARD); + } + override doEvaluate(operandValue: string): string | undefined { + return operandValue; + } +} + +export class AsDecimalOperatorEvaluator extends DefaultUnaryOperatorEvaluator< + string, + number +> { + constructor() { + super('asDecimal', STRING_TYPEGUARD); + } + override doEvaluate(operandValue: string): number { + return Number.parseFloat(operandValue); + } +} + +export class AsIntegerOperatorEvaluator extends DefaultUnaryOperatorEvaluator< + string, + number +> { + constructor() { + super('asInteger', STRING_TYPEGUARD); + } + override doEvaluate(operandValue: string): number { + return Number.parseInt(operandValue, 10); + } +} + +export class AsBooleanOperatorEvaluator extends DefaultUnaryOperatorEvaluator< + string, + boolean +> { + constructor() { + super('asBoolean', STRING_TYPEGUARD); + } + override doEvaluate(operandValue: string): boolean { + return operandValue === 'true'; + } +} 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..c2d29aa10 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(), + asInteger: new AsIntegerOperatorEvaluator(), + asBoolean: new AsBooleanOperatorEvaluator(), + asText: new AsTextOperatorEvaluator(), }; binary = { pow: new PowOperatorEvaluator(), @@ -133,6 +149,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; + } +} From 1bfdc867c3bcb1e4680fcdc2e030f1c884ab2949 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Thu, 16 May 2024 13:32:24 +0200 Subject: [PATCH 3/4] refractor(operators): Move parsing logic to Valuetype --- .../internal-representation-parsing.ts | 51 +++++-------------- .../src/grammar/expression.langium | 1 - .../evaluators/parse-operators-evaluators.ts | 25 ++++----- .../lib/ast/expressions/operator-registry.ts | 10 ++-- .../src/lib/ast/expressions/typeguards.ts | 11 ++++ .../primitive/boolean-value-type.ts | 12 +++++ .../primitive/decimal-value-type.ts | 14 +++++ .../primitive/integer-value-type.ts | 23 ++++++++- .../primitive/primitive-value-type.ts | 5 ++ .../value-type/primitive/text-value-type.ts | 4 ++ libs/language-server/src/lib/jayvee-module.ts | 3 +- 11 files changed, 101 insertions(+), 58 deletions(-) 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/language-server/src/grammar/expression.langium b/libs/language-server/src/grammar/expression.langium index 17efe09ca..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)*; 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 index 4cc5e8842..e1f07c4de 100644 --- 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 @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only +import { type ValueTypeProvider } from '../../wrappers'; import { DefaultUnaryOperatorEvaluator } from '../operator-evaluator'; import { STRING_TYPEGUARD } from '../typeguards'; @@ -9,11 +10,11 @@ export class AsTextOperatorEvaluator extends DefaultUnaryOperatorEvaluator< string, string > { - constructor() { + constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asText', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): string | undefined { - return operandValue; + override doEvaluate(operandValue: string): string { + return this.valueTypeProvider.Primitives.Text.fromString(operandValue); } } @@ -21,11 +22,11 @@ export class AsDecimalOperatorEvaluator extends DefaultUnaryOperatorEvaluator< string, number > { - constructor() { + constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asDecimal', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): number { - return Number.parseFloat(operandValue); + override doEvaluate(operandValue: string): number | undefined { + return this.valueTypeProvider.Primitives.Decimal.fromString(operandValue); } } @@ -33,11 +34,11 @@ export class AsIntegerOperatorEvaluator extends DefaultUnaryOperatorEvaluator< string, number > { - constructor() { + constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asInteger', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): number { - return Number.parseInt(operandValue, 10); + override doEvaluate(operandValue: string): number | undefined { + return this.valueTypeProvider.Primitives.Integer.fromString(operandValue); } } @@ -45,10 +46,10 @@ export class AsBooleanOperatorEvaluator extends DefaultUnaryOperatorEvaluator< string, boolean > { - constructor() { + constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asBoolean', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): boolean { - return operandValue === 'true'; + override doEvaluate(operandValue: string): boolean | undefined { + return this.valueTypeProvider.Primitives.Boolean.fromString(operandValue); } } 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 c2d29aa10..5179ef689 100644 --- a/libs/language-server/src/lib/ast/expressions/operator-registry.ts +++ b/libs/language-server/src/lib/ast/expressions/operator-registry.ts @@ -106,10 +106,10 @@ export class DefaultOperatorEvaluatorRegistry round: new RoundOperatorEvaluator(), lowercase: new LowercaseOperatorEvaluator(), uppercase: new UppercaseOperatorEvaluator(), - asDecimal: new AsDecimalOperatorEvaluator(), - asInteger: new AsIntegerOperatorEvaluator(), - asBoolean: new AsBooleanOperatorEvaluator(), - asText: new AsTextOperatorEvaluator(), + asDecimal: new AsDecimalOperatorEvaluator(this.valueTypeProvider), + asInteger: new AsIntegerOperatorEvaluator(this.valueTypeProvider), + asBoolean: new AsBooleanOperatorEvaluator(this.valueTypeProvider), + asText: new AsTextOperatorEvaluator(this.valueTypeProvider), }; binary = { pow: new PowOperatorEvaluator(), @@ -134,6 +134,8 @@ export class DefaultOperatorEvaluatorRegistry ternary = { replace: new ReplaceOperatorEvaluator(), }; + + constructor(private readonly valueTypeProvider: ValueTypeProvider) {} } export class DefaultOperatorTypeComputerRegistry 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) => From fec463e12a0cc6df4967a918ec363bc81792243e Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Thu, 16 May 2024 17:16:55 +0200 Subject: [PATCH 4/4] logging(operators): Improve log messages Evaluators now throw Errors on failure. This commit also adapts the tests and fixes typos --- .../valid-column-type-change.jv | 4 +- .../parse-operators-evaluators.spec.ts | 134 ++++++------------ .../evaluators/parse-operators-evaluators.ts | 27 +++- 3 files changed, 69 insertions(+), 96 deletions(-) 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 e8ed6f7e4..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 @@ -5,9 +5,9 @@ pipeline TestPipeline { transform ToBool { from integer oftype integer; - to bool oftype boolean; + to boolean oftype boolean; - bool: integer != 0; + boolean: integer != 0; } block TestExtractor oftype TestTableExtractor { 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 index b8e315e8c..5f6e56de2 100644 --- 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 @@ -1,132 +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'; -describe('The asText operator', () => { - it('should parse text successfully', async () => { - const result = await executeDefaultTextToTextExpression( - 'asText inputValue', - 'someText', +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(); + } +} - expect(result).toEqual('someText'); +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 () => { - const result = await executeDefaultTextToTextExpression( - 'asDecimal inputValue', - '1.6', - ); - - expect(result).toEqual(1.6); + await expectSuccess('asDecimal', '1.6', 1.6); }); it('should parse decimals with commas successfully', async () => { - const result = await executeDefaultTextToTextExpression( - 'asDecimal inputValue', - '1,6', - ); - - expect(result).toEqual(1.6); + await expectSuccess('asDecimal', '1,6', 1.6); }); it('should parse negative decimals with commas successfully', async () => { - const result = await executeDefaultTextToTextExpression( - 'asDecimal inputValue', - '-1,6', - ); - - expect(result).toEqual(-1.6); + await expectSuccess('asDecimal', '-1,6', -1.6); }); }); describe('The asInteger operator', () => { it('should parse positive integers successfully', async () => { - const result = await executeDefaultTextToTextExpression( - 'asInteger inputValue', - '32', - ); - - expect(result).toEqual(32); + await expectSuccess('asInteger', '32', 32); }); it('should parse negative integers successfully', async () => { - const result = await executeDefaultTextToTextExpression( - 'asInteger inputValue', - '-1', - ); - - expect(result).toEqual(-1); + await expectSuccess('asInteger', '-1', -1); }); it('should fail with decimal values', async () => { - const result = await executeDefaultTextToTextExpression( - 'asInteger inputValue', - '32.5', - ); - - expect(result).toEqual(undefined); + await expectError('asInteger', '32.5'); }); }); describe('The asBoolean operator', () => { it('should parse true and True successfully', async () => { - let result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - 'true', - ); - - expect(result).toEqual(true); - - result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - 'True', - ); - - expect(result).toEqual(true); + await expectSuccess('asBoolean', 'true', true); + await expectSuccess('asBoolean', 'True', true); }); it('should parse false and False successfully', async () => { - let result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - 'false', - ); - - expect(result).toEqual(false); - - result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - 'False', - ); - - expect(result).toEqual(false); + await expectSuccess('asBoolean', 'false', false); + await expectSuccess('asBoolean', 'False', false); }); it('should fail with 0 and 1', async () => { - let result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - '0', - ); - - expect(result).toEqual(undefined); - - result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - '1', - ); - - expect(result).toEqual(undefined); + await expectError('asBoolean', '0'); + await expectError('asBoolean', '1'); }); - it('should fail on a random string', async () => { - const result = await executeDefaultTextToTextExpression( - 'asBoolean inputValue', - 'notABoolean', - ); - - expect(result).toEqual(undefined); + 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 index e1f07c4de..920b243e1 100644 --- 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 @@ -25,8 +25,13 @@ export class AsDecimalOperatorEvaluator extends DefaultUnaryOperatorEvaluator< constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asDecimal', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): number | undefined { - return this.valueTypeProvider.Primitives.Decimal.fromString(operandValue); + 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; } } @@ -37,8 +42,13 @@ export class AsIntegerOperatorEvaluator extends DefaultUnaryOperatorEvaluator< constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asInteger', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): number | undefined { - return this.valueTypeProvider.Primitives.Integer.fromString(operandValue); + 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; } } @@ -49,7 +59,12 @@ export class AsBooleanOperatorEvaluator extends DefaultUnaryOperatorEvaluator< constructor(private readonly valueTypeProvider: ValueTypeProvider) { super('asBoolean', STRING_TYPEGUARD); } - override doEvaluate(operandValue: string): boolean | undefined { - return this.valueTypeProvider.Primitives.Boolean.fromString(operandValue); + 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; } }