Skip to content

Commit

Permalink
feat(bettermath): Add ref support to lazy evaluation of values, depen…
Browse files Browse the repository at this point in the history
…ding on external ref-value maps
  • Loading branch information
imnotteixeira committed Dec 3, 2023
1 parent fb1e057 commit f9bf92f
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 129 deletions.
File renamed without changes.
10 changes: 9 additions & 1 deletion src/grammar/bettermath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
NumberType,
IValueType,
BettermathGrammarParser,
IRefType,
RefType,
} from "./definitions";
import { FunctionType, IFunctionArg } from "./functions/types";
import { FunctionRegistry } from "./functions";
Expand Down Expand Up @@ -262,7 +264,13 @@ const buildGrammar = (functionRegistry: FunctionRegistry): BettermathGrammarPars
(str: string, indexInfo: Index) => new NumberType(indexInfo, str)
).desc("number");

const ExpressionValue = P.alt<IValueType<number> | IValueType<string>>(Num, QuotedString);
const Ref: P.Parser<IRefType> = P.seqMap<string, Index, IRefType>(
P.regexp(/[A-Z]+[1-9]+[0-9]*/).notFollowedBy(Word),
P.index,
(str: string, indexInfo: Index) => new RefType(indexInfo, str)
).desc("ref");

const ExpressionValue = P.alt<IValueType<number> | IValueType<string>>(Num, QuotedString, Ref);
const ImmediateValue = P.alt<IValueType<number> | IValueType<string>>(
Num.notFollowedBy(RawString),
RawString,
Expand Down
67 changes: 63 additions & 4 deletions src/grammar/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,41 @@ export enum Types {

type AllowedType = Types;

export class ValueResolvingResult<T> {
readonly isError: boolean;
readonly value?: T;
readonly error?: Error;

private constructor(isError: boolean, result?: {value? : T, error?: Error}) {
this.isError = isError;
this.value = result?.value;
this.error = result?.error;
}

static success = <T>(value: T) => new ValueResolvingResult(false, {value});

static error = <T>(error: Error) => new ValueResolvingResult<T>(true, {error});

get = () => {
if (this.isError) {
throw new Error(
"The ValueResolvingResult is an error. Cannot get value. Inner error is: "
+ this.error
);
}

return this.value as T;
}

getOrElse = (elseValue: T) => this.isError ? elseValue : this.value;

getError = () => this.error;
}

export interface IBaseType<T> {
type: AllowedType;
indexInfo: Index;
getValue: () => T;
getValue: (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => ValueResolvingResult<T>;

validate: () => ValidationResult;

Expand All @@ -28,7 +59,7 @@ export abstract class BaseType<T> implements IBaseType<T> {
abstract type: AllowedType;
readonly indexInfo: Index;

abstract getValue: () => T;
abstract getValue: (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => ValueResolvingResult<T>;
abstract validate: () => ValidationResult;

constructor(indexInfo: Index) {
Expand Down Expand Up @@ -69,7 +100,7 @@ export class StringType extends BaseType<string> implements IStringType {
this.value = str;
}

getValue = () => this.value;
getValue = () => ValueResolvingResult.success(this.value);
validate = () => makeSuccess();
}

Expand All @@ -87,7 +118,35 @@ export class NumberType extends BaseType<number> implements INumberType {
this.value = Number(num);
}

getValue = () => this.value;
getValue = () => ValueResolvingResult.success(this.value);
validate = () => makeSuccess();
}

export interface IRefType extends IBaseType<any|undefined> {
type: Types.REF;
value: any| undefined;
}

export class RefType extends BaseType<any | undefined> implements IRefType {
readonly type = Types.REF;
readonly value: string;

constructor(indexInfo: Index, refId: string) {
super(indexInfo);
this.value = refId;
}

getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
const referencedElem = dependencyValueMap.get(this.value);
if (!referencedElem) {
// Returning undefined value as success, since some functions may take advanatage of this, even though the value does not exist.
// Specific ValueResolvingResult.error will be returned if some function is processing an undefined Ref when it can't
return ValueResolvingResult.success(undefined)
} else {
return referencedElem.getValue(dependencyValueMap)
}
}

validate = () => makeSuccess();
}

Expand Down
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Add.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { ValidationError } from "../validator";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class AddFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,14 @@ export class AddFunction extends FunctionType<number> {
super(indexInfo, "Add", args);
}

getValue = () => this.args[0].getValue() + this.args[1].getValue();
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers(this.args.map(arg => arg.getValue(dependencyValueMap)))
return ValueResolvingResult.success(resolvedValues[0] + resolvedValues[1]);
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

protected validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (_: ValidationError[]) => void) => {

Expand Down
14 changes: 11 additions & 3 deletions src/grammar/functions/base/Concat.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionType, IFunctionArg } from "../types";

export class ConcatFunction extends FunctionType<string> {
readonly returnType = Types.STRING;
constructor(indexInfo: Index, args: IFunctionArg<string | number>[]) {
constructor(indexInfo: Index, args: (IFunctionArg<string> | IFunctionArg<number>)[]) {
super(indexInfo, "CONCAT", args);
}

getValue = () => this.args.reduce((acc, arg) => acc + arg.getValue(), "");
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
return ValueResolvingResult.success(
this.args.reduce((acc, arg) => acc + arg.getValue(dependencyValueMap).getOrElse(""), "")
)
} catch (e) {
return ValueResolvingResult.error<string>(e as Error)
}
}
}
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Divide.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { ValidationError } from "../validator";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class DivideFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,14 @@ export class DivideFunction extends FunctionType<number> {
super(indexInfo, "Divide", args);
}

getValue = () => this.args[0].getValue() / this.args[1].getValue();
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers(this.args.map(arg => arg.getValue(dependencyValueMap)))
return ValueResolvingResult.success(resolvedValues[0] / resolvedValues[1]);
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

protected validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (_: ValidationError[]) => void) => {

Expand Down
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Exponentiate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { ValidationError } from "../validator";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class ExponentiateFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,14 @@ export class ExponentiateFunction extends FunctionType<number> {
super(indexInfo, "Exponentiate", args);
}

getValue = () => Math.pow(this.args[0].getValue(), this.args[1].getValue());
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers(this.args.map(arg => arg.getValue(dependencyValueMap)))
return ValueResolvingResult.success(Math.pow(resolvedValues[0], resolvedValues[1]));
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

protected validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (_: ValidationError[]) => void) => {

Expand Down
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Factorial.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { ValidationError } from "../validator";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class FactorialFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,14 @@ export class FactorialFunction extends FunctionType<number> {
super(indexInfo, "Factorial", args);
}

getValue = () => this.calculateFactorial(this.args[0].getValue());
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers([this.args[0].getValue(dependencyValueMap)])
return ValueResolvingResult.success(this.calculateFactorial(resolvedValues[0]));
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (errors: ValidationError[]) => void) => {

Expand Down
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Multiply.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { ValidationError } from "../validator";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class MultiplyFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
constructor(indexInfo: Index, args: IFunctionArg<any>[]) {
super(indexInfo, "Multiply", args);
}

getValue = () => this.args[0].getValue() * this.args[1].getValue();
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers(this.args.map(arg => arg.getValue(dependencyValueMap)))
return ValueResolvingResult.success(resolvedValues[0] * resolvedValues[1]);
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

protected validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (_: ValidationError[]) => void) => {

Expand Down
14 changes: 12 additions & 2 deletions src/grammar/functions/base/Negate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { ValidationError } from "../validator";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class NegateFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,16 @@ export class NegateFunction extends FunctionType<number> {
super(indexInfo, "Negate", args);
}

getValue = () => -this.args[0].getValue();

getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers([this.args[0].getValue(dependencyValueMap)])
return ValueResolvingResult.success(-resolvedValues[0]);
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}


validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (errors: ValidationError[]) => void) => {

Expand Down
12 changes: 10 additions & 2 deletions src/grammar/functions/base/Subtract.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Index } from "parsimmon";
import { Types } from "../../definitions";
import { IBaseType, Types, ValueResolvingResult } from "../../definitions";
import { FunctionArgsValidator, FunctionType, IFunctionArg } from "../types";
import { ValidationError } from "../validator";
import { CommonValidators, PipelineValidator } from "../validator/pipeline";
import { is } from "../validator/argTypeValidator";
import { resolveValuesOnlyNumbers } from "../util";

export class SubtractFunction extends FunctionType<number> {
readonly returnType = Types.NUMBER;
Expand All @@ -12,7 +13,14 @@ export class SubtractFunction extends FunctionType<number> {
super(indexInfo, "Subtract", args);
}

getValue = () => this.args[0].getValue() - this.args[1].getValue();
getValue = (dependencyValueMap: Map<string, IBaseType<any> | undefined>) => {
try {
const resolvedValues = resolveValuesOnlyNumbers(this.args.map(arg => arg.getValue(dependencyValueMap)))
return ValueResolvingResult.success(resolvedValues[0] - resolvedValues[1]);
} catch (e) {
return ValueResolvingResult.error<number>(e as Error)
}
}

protected validateArgs: FunctionArgsValidator = (validator: PipelineValidator, args: IFunctionArg<any>[], onSuccess: () => void, onFailure: (_: ValidationError[]) => void) => {

Expand Down
17 changes: 17 additions & 0 deletions src/grammar/functions/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ValueResolvingResult } from "../../definitions"

export const resolveValuesOnlyNumbers = <T>(values: ValueResolvingResult<T>[]) => {

const anyError = values.find(val => val.isError)

if (anyError) {
throw anyError.getError()
}

const resolvedValues = values.map(result => result.get())
if (resolvedValues.some(val => !Number.isFinite(val))) {
throw new Error("Arguments must be numbers.")
}

return resolvedValues
}
Loading

0 comments on commit f9bf92f

Please sign in to comment.