Skip to content

Commit

Permalink
GUS-3834 Add division support + improve tests and type assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
atoledo committed Aug 2, 2024
1 parent 9852213 commit 3a9a55c
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 29 deletions.
42 changes: 26 additions & 16 deletions src/__test__/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,16 @@ describe('Condition Engine', () => {
).toThrowError()
})

test.each<[ExpressionInput]>([[['+', 5, 5]], [['-', 5, 5]], [['*', 5, 5]]])(
'%p should throw',
(expression) => {
expect(() => engine.evaluate(expression, {})).toThrowError(
'non expression or boolean result should be returned'
)
}
)
test.each<[ExpressionInput]>([
[['+', 5, 5]],
[['-', 5, 5]],
[['*', 5, 5]],
[['/', 5, 5]],
])('%p should throw', (expression) => {
expect(() => engine.evaluate(expression, {})).toThrowError(
'non expression or boolean result should be returned'
)
})
})

test('statement', () => {
Expand Down Expand Up @@ -232,6 +234,12 @@ describe('Condition Engine', () => {
[['>', ['*', '$a', 6], 6], { a: 1.1 }, true],
[['>', ['*', '$a', 5], 6], { a: 1 }, false],
[['==', ['*', '$a', 1, 2, 3], 30], { a: 5 }, true],
[['>', ['/', '$a', 6], 6], { a: 42 }, true],
[['>', ['/', '$a', 5], 6], { a: 30 }, false],
[['==', ['/', '$a', 3, 2, 1], 15], { a: 90 }, true],
[['>', ['/', 10, 0], 10000], {}, true], // 10 / 0 = Infinity
[['>', 10000, ['/', 10, 0]], {}, false], // 10 / 0 = Infinity
[['>', ['/', 0, 0], 10000], {}, false], // 0 / 0 = NaN
])(
'%p with context %p should be simplified to %p',
(
Expand All @@ -248,13 +256,15 @@ describe('Condition Engine', () => {
}
)

test.each<[ExpressionInput]>([[['+', 5, 5]], [['-', 5, 5]], [['*', 5, 5]]])(
'%p should throw',
(expression) => {
expect(() => engine.simplify(expression, {})).toThrowError(
'non expression or boolean result should be returned'
)
}
)
test.each<[ExpressionInput]>([
[['+', 5, 5]],
[['-', 5, 5]],
[['*', 5, 5]],
[['/', 5, 5]],
])('%p should throw', (expression) => {
expect(() => engine.simplify(expression, {})).toThrowError(
'non expression or boolean result should be returned'
)
})
})
})
60 changes: 57 additions & 3 deletions src/common/__test__/unit/type-check.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { isNumber, isObject, isString } from '../../type-check'
import { operand } from '../../../__test__/helpers'
import { Evaluable, Result } from '../../evaluable'
import {
areAllNumbers,
areAllResults,
isBoolean,
isNumber,
isObject,
isString,
} from '../../type-check'

describe('Common - Type Check', () => {
describe('isNumber', () => {
test.each([
// Truthy
[1, true],
[1.0, true],
[Infinity, true],
[-Infinity, true],
[NaN, true],
// Falsy
[Infinity, false],
[-Infinity, false],
['1', false],
[true, false],
[false, false],
Expand Down Expand Up @@ -52,4 +62,48 @@ describe('Common - Type Check', () => {
expect(isObject(value)).toBe(expected)
})
})

describe('isBoolean', () => {
test.each([
// Truthy
[true, true],
[false, true],
// Falsy
[{}, false],
['hi', false],
[1, false],
[null, false],
[undefined, false],
])('%p should evaluate as %p', (value, expected) => {
expect(isBoolean(value)).toBe(expected)
})
})

describe('areAllResults', () => {
test.each<[(Result | Evaluable)[], boolean]>([
// Truthy
[[1, 2, 3], true],
[[1, '2', 3], true],
[[1, 2, {}], true],
[[1, 2, '3'], true],
// Falsy
[[1, 2, '3', operand(1)], false],
])('if %p should evaluate as %p', (values, expectedResult) => {
const result = areAllResults(values)
expect(result).toEqual(expectedResult)
})
})
describe('areAllNumbers', () => {
test.each<[Result[], boolean]>([
// Truthy
[[1, 2, 3], true],
// Falsy
[[1, '2', 3], false],
[[1, 2, {}], false],
[[1, 2, '3'], false],
])('if %p should evaluate as %p', (values, expectedResult) => {
const result = areAllNumbers(values)
expect(result).toEqual(expectedResult)
})
})
})
2 changes: 1 addition & 1 deletion src/common/type-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Evaluable, Result } from './evaluable'
* @param value Tested value.
*/
export function isNumber(value: Result): value is number {
return typeof value === 'number' && isFinite(value)
return typeof value === 'number'
}

/**
Expand Down
106 changes: 106 additions & 0 deletions src/expression/arithmetic/__test__/unit/divide.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { notSimplified, operand } from '../../../../__test__/helpers'
import { Result } from '../../../../common/evaluable'
import { Operand } from '../../../../operand'
import { Value } from '../../../../operand/value'
import { defaultOptions } from '../../../../parser/options'
import { Divide, OPERATOR } from '../../divide'

describe('Expression - Arithmetic - Divide', () => {
describe('constructor', () => {
test.each([[[]], [[operand(5)]]])('arguments %p should throw', (args) => {
expect(() => new Divide(...args)).toThrowError(
'divide expression requires at least 2 operands'
)
})
})

const testCases: [Result, ...Operand[]][] = [
[2, operand(20), operand(10)],
[0.016666666666666666, operand(10), operand(20), operand(30)],
[-0.5, operand(10), operand(-20)],
[-0.5, operand(-10), operand(20)],
[0.5, operand(-10), operand(-20)],
[Infinity, operand(10), operand(0)],
[-0, operand(0), operand(-10)],
[NaN, operand(0), operand(0)],
[2.3333333333333335, operand(2.8), operand(1.2)],
[2.0999999099999913, operand(2.333333), operand(1.111111)],
[4, operand(0.4), operand(0.1)],
[1, operand(1.333), operand(1.333)],
]

describe('evaluate', () => {
test.each(testCases)(
'that %p is the result of divideing %p',
(expected, ...operands) => {
expect(new Divide(...operands).evaluate({})).toBe(expected)
}
)
})

describe('evaluate - failure', () => {
it.each<[...Operand[]]>([
[operand('string1'), operand(2)],
[operand('string1'), operand('string2')],
[operand(1), operand('string2')],
[operand(null), operand(1)],
[operand(undefined), operand(1)],
])('%p and %p should throw', (...operands) => {
expect(() => new Divide(...operands).evaluate({})).toThrowError()
})
})

describe('toString', () => {
it.each<[string, ...Value[]]>([
['(5 / 6)', new Value(5), new Value(6)],
[
'(1 / 2 / 3 / 4)',
new Value(1),
new Value(2),
new Value(3),
new Value(4),
],
['(5 / -6)', new Value(5), new Value(-6)],
])('should stringify into %p', (expectedResult, ...values) => {
const result = new Divide(...values).toString()
expect(result).toEqual(expectedResult)
})
})

describe('simplify', () => {
test.each<[Result | 'self', ...Operand[]]>([
['self', operand(10), notSimplified()],
['self', notSimplified(), operand(10)],
['self', notSimplified(), notSimplified()],
...testCases,
])('if %p is the simplification of %p', (expected, ...operands) => {
const division = new Divide(...operands)
const result = division.simplify({}, [])
if (expected === 'self') {
expect(result).toBe(division)
} else {
expect(result).toEqual(expected)
}
})
})

describe('serialize', () => {
it('%p and %p should be serialized to %p', () => {
expect(
new Divide(new Value(10), new Value(20)).serialize({
...defaultOptions,
operatorMapping: new Map([[OPERATOR, 'CUSTOM_DIVIDE']]),
})
).toEqual(['CUSTOM_DIVIDE', 10, 20])
})

it('should throw if operator symbol is not mapped', () => {
expect(() =>
new Divide(new Value(10), new Value(20)).serialize({
...defaultOptions,
operatorMapping: new Map<symbol, string>(),
})
).toThrowError()
})
})
})
4 changes: 2 additions & 2 deletions src/expression/arithmetic/__test__/unit/multiply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ describe('Expression - Arithmetic - Multiply', () => {
expect(
new Multiply(new Value(10), new Value(20)).serialize({
...defaultOptions,
operatorMapping: new Map([[OPERATOR, 'CUSTOM_MULTRACT']]),
operatorMapping: new Map([[OPERATOR, 'CUSTOM_MULT']]),
})
).toEqual(['CUSTOM_MULTRACT', 10, 20])
).toEqual(['CUSTOM_MULT', 10, 20])
})

it('should throw if operator symbol is not mapped', () => {
Expand Down
38 changes: 38 additions & 0 deletions src/expression/arithmetic/divide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Evaluable, Result } from '../../common/evaluable'
import { areAllNumbers } from '../../common/type-check'
import { Operand } from '../../operand'
import { Arithmetic } from '.'

// Operator key
export const OPERATOR = Symbol('DIVIDE')

/**
* Divide operation expression
*/
export class Divide extends Arithmetic {
/**
* @constructor Generic constructor
* @param {Evaluable[]} args
*/
constructor(...args: Evaluable[])
/**
* @constructor
* @param {Operand[]} operands Operands.
*/
constructor(...operands: Operand[]) {
if (operands.length < 2) {
throw new Error('divide expression requires at least 2 operands')
}
super('/', OPERATOR, operands)
}

operate(results: Result[]): Result {
if (!areAllNumbers(results)) {
throw new Error('operands must be numbers for divide')
}

const result = results.reduce((acc, result) => acc / result)

return result
}
}
2 changes: 1 addition & 1 deletion src/expression/comparison/ge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class GreaterThanOrEqual extends Comparison {
*/
comparison(left: Result, right: Result): boolean {
if (isNumber(left) && isNumber(right)) {
return (left as number) >= (right as number)
return left >= right
}

const leftDate = toDateNumber(left),
Expand Down
2 changes: 1 addition & 1 deletion src/expression/comparison/gt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class GreaterThan extends Comparison {
*/
comparison(left: Result, right: Result): boolean {
if (isNumber(left) && isNumber(right)) {
return (left as number) > (right as number)
return left > right
}

const leftDate = toDateNumber(left),
Expand Down
2 changes: 1 addition & 1 deletion src/expression/comparison/le.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class LessThanOrEqual extends Comparison {
*/
comparison(left: Result, right: Result): boolean {
if (isNumber(left) && isNumber(right)) {
return (left as number) <= (right as number)
return left <= right
}

const leftDate = toDateNumber(left),
Expand Down
2 changes: 1 addition & 1 deletion src/expression/comparison/lt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class LessThan extends Comparison {
*/
comparison(left: Result, right: Result): boolean {
if (isNumber(left) && isNumber(right)) {
return (left as number) < (right as number)
return left < right
}

const leftDate = toDateNumber(left),
Expand Down
15 changes: 12 additions & 3 deletions src/parser/__test__/unit/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Evaluable } from '../../../common/evaluable'
import {
Divide,
OPERATOR as OPERATOR_DIVIDE,
} from '../../../expression/arithmetic/divide'
import {
Multiply,
OPERATOR as OPERATOR_MULTIPLY,
Expand Down Expand Up @@ -167,6 +171,10 @@ describe('Condition Engine - Parser', () => {
[defaultOptions.operatorMapping.get(OPERATOR_MULTIPLY), 5, 5],
new Multiply(new Value(5), new Value(5)),
],
[
[defaultOptions.operatorMapping.get(OPERATOR_DIVIDE), 5, 5],
new Divide(new Value(5), new Value(5)),
],
] as [ExpressionInput, Evaluable][])(
'%p should evaluate as %p',
(expression, expected) => {
Expand Down Expand Up @@ -496,6 +504,10 @@ describe('Condition Engine - Parser', () => {
[defaultOptions.operatorMapping.get(OPERATOR_MULTIPLY), 5, 5],
new Multiply(new Value(5), new Value(5)),
],
[
[defaultOptions.operatorMapping.get(OPERATOR_DIVIDE), 5, 5],
new Divide(new Value(5), new Value(5)),
],
] as [ExpressionInput, Evaluable][])(
'%p should evaluate as %p',
(expression, expected) => {
Expand All @@ -510,9 +522,6 @@ describe('Condition Engine - Parser', () => {
[[]],
// Invalid operator
[['__', 5, 5]],

// TODO
[['/', 5, 5]],
] as [ExpressionInput][])('%p should throw', (expression) => {
expect(() => parser.parse(expression)).toThrowError()
})
Expand Down
Loading

0 comments on commit 3a9a55c

Please sign in to comment.