From 6ea9740a45e692831512707c6ab43a78d97a81ba Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 20:35:57 -0700 Subject: [PATCH 1/6] refactor: change calc to use a class based imp --- packages/utils/src/calc/calc.test.ts | 159 ++++++++++++++++++++ packages/utils/src/calc/calc.ts | 211 +++++++++++++++++++++++++++ packages/utils/src/calc/index.ts | 1 + packages/utils/src/index.test.ts | 144 ------------------ packages/utils/src/index.ts | 61 +------- 5 files changed, 372 insertions(+), 204 deletions(-) create mode 100644 packages/utils/src/calc/calc.test.ts create mode 100644 packages/utils/src/calc/calc.ts create mode 100644 packages/utils/src/calc/index.ts delete mode 100644 packages/utils/src/index.test.ts diff --git a/packages/utils/src/calc/calc.test.ts b/packages/utils/src/calc/calc.test.ts new file mode 100644 index 000000000..9c0f11710 --- /dev/null +++ b/packages/utils/src/calc/calc.test.ts @@ -0,0 +1,159 @@ +import { calc } from './calc'; + +const css = 'var(--some-css-var)'; + +describe('class UnitCalc', () => { + describe('add()', () => { + it.each` + args | expectedExpressions | expectedBuilt + ${[]} | ${['1']} | ${'calc(1)'} + ${[2]} | ${['1', '2']} | ${'calc(1 + 2)'} + ${[2, '3']} | ${['1', '2', '3']} | ${'calc(1 + 2 + 3)'} + ${[calc(2)]} | ${['1', '2']} | ${'calc(1 + 2)'} + ${[calc(2).add(3)]} | ${['1', '2 + 3']} | ${'calc(1 + 2 + 3)'} + ${[calc(2).subtract(3)]} | ${['1', '2 - 3']} | ${'calc(1 + 2 - 3)'} + ${[calc(2).multiply(3)]} | ${['1', '2 * 3']} | ${'calc(1 + 2 * 3)'} + ${[calc(2).divide(3)]} | ${['1', '2 / 3']} | ${'calc(1 + 2 / 3)'} + ${[css]} | ${['1', css]} | ${'calc(1 + var(--some-css-var))'} + `( + 'should add $args to 1 to get $expectedBuilt', + ({ args, expectedExpressions, expectedBuilt }) => { + const result = calc(1).add(...args); + + expect(result).toMatchObject({ + expressions: expectedExpressions, + operator: '+', + }); + expect(result.build()).toBe(expectedBuilt); + }, + ); + }); + + describe('subtract()', () => { + it.each` + args | expectedExpressions | expectedBuilt + ${[]} | ${['1']} | ${'calc(1)'} + ${[2]} | ${['1', '2']} | ${'calc(1 - 2)'} + ${[2, '3']} | ${['1', '2', '3']} | ${'calc(1 - 2 - 3)'} + ${[calc(2)]} | ${['1', '2']} | ${'calc(1 - 2)'} + ${[calc(2).add(3)]} | ${['1', '2 + 3']} | ${'calc(1 - 2 + 3)'} + ${[calc(2).subtract(3)]} | ${['1', '2 - 3']} | ${'calc(1 - 2 - 3)'} + ${[calc(2).multiply(3)]} | ${['1', '2 * 3']} | ${'calc(1 - 2 * 3)'} + ${[calc(2).divide(3)]} | ${['1', '2 / 3']} | ${'calc(1 - 2 / 3)'} + ${[css]} | ${['1', css]} | ${'calc(1 - var(--some-css-var))'} + `( + 'should subtract $args from 1 to get $expectedBuilt', + ({ args, expectedExpressions, expectedBuilt }) => { + const result = calc(1).subtract(...args); + + expect(result).toMatchObject({ + expressions: expectedExpressions, + operator: '-', + }); + expect(result.build()).toBe(expectedBuilt); + }, + ); + }); + + describe('multiply()', () => { + it.each` + args | expectedExpressions | expectedBuilt + ${[]} | ${['1']} | ${'calc(1)'} + ${[2]} | ${['1', '2']} | ${'calc(1 * 2)'} + ${[2, '3']} | ${['1', '2', '3']} | ${'calc(1 * 2 * 3)'} + ${[calc(2)]} | ${['1', '2']} | ${'calc(1 * 2)'} + ${[calc(2).add(3)]} | ${['1', '(2 + 3)']} | ${'calc(1 * (2 + 3))'} + ${[calc(2).subtract(3)]} | ${['1', '(2 - 3)']} | ${'calc(1 * (2 - 3))'} + ${[calc(2).multiply(3)]} | ${['1', '2 * 3']} | ${'calc(1 * 2 * 3)'} + ${[calc(2).divide(3)]} | ${['1', '2 / 3']} | ${'calc(1 * 2 / 3)'} + ${[css]} | ${['1', css]} | ${'calc(1 * var(--some-css-var))'} + `( + 'should multiply 1 by $args to get $expectedBuilt', + ({ args, expectedExpressions, expectedBuilt }) => { + const result = calc(1).multiply(...args); + + expect(result).toMatchObject({ + expressions: expectedExpressions, + operator: '*', + }); + expect(result.build()).toBe(expectedBuilt); + }, + ); + }); + + describe('divide()', () => { + it.each` + args | expectedExpressions | expectedBuilt + ${[]} | ${['1']} | ${'calc(1)'} + ${[2]} | ${['1', '2']} | ${'calc(1 / 2)'} + ${[2, '3']} | ${['1', '2', '3']} | ${'calc(1 / 2 / 3)'} + ${[calc(2)]} | ${['1', '2']} | ${'calc(1 / 2)'} + ${[calc(2).add(3)]} | ${['1', '(2 + 3)']} | ${'calc(1 / (2 + 3))'} + ${[calc(2).subtract(3)]} | ${['1', '(2 - 3)']} | ${'calc(1 / (2 - 3))'} + ${[calc(2).multiply(3)]} | ${['1', '2 * 3']} | ${'calc(1 / 2 * 3)'} + ${[calc(2).divide(3)]} | ${['1', '2 / 3']} | ${'calc(1 / 2 / 3)'} + ${[css]} | ${['1', css]} | ${'calc(1 / var(--some-css-var))'} + `( + 'should divide 1 by $args to get $expectedBuilt', + ({ args, expectedExpressions, expectedBuilt }) => { + const result = calc(1).divide(...args); + + expect(result).toMatchObject({ + expressions: expectedExpressions, + operator: '/', + }); + expect(result.build()).toBe(expectedBuilt); + }, + ); + }); + + describe('negate()', () => { + it.each` + calc | expectedExpressions | expectedBuilt + ${calc(1)} | ${['1', '-1']} | ${'calc(1 * -1)'} + ${calc(1).add(3)} | ${['(1 + 3)', '-1']} | ${'calc((1 + 3) * -1)'} + ${calc(1).subtract(3)} | ${['(1 - 3)', '-1']} | ${'calc((1 - 3) * -1)'} + ${calc(1).multiply(3)} | ${['1 * 3', '-1']} | ${'calc(1 * 3 * -1)'} + ${calc(1).divide(3)} | ${['1 / 3', '-1']} | ${'calc(1 / 3 * -1)'} + ${calc(css)} | ${[css, '-1']} | ${'calc(var(--some-css-var) * -1)'} + `( + 'should negate $calc to get $expectedBuilt', + ({ calc, expectedExpressions, expectedBuilt }) => { + const result = calc.negate(); + + expect(result).toMatchObject({ + expressions: expectedExpressions, + operator: '*', + }); + expect(result.build()).toBe(expectedBuilt); + }, + ); + }); + + describe('build()', () => { + it.each` + calc | expected + ${calc(1)} | ${'calc(1)'} + ${calc(1).add(3)} | ${'calc(1 + 3)'} + ${calc(1).subtract(3)} | ${'calc(1 - 3)'} + ${calc(1).multiply(3)} | ${'calc(1 * 3)'} + ${calc(1).divide(3)} | ${'calc(1 / 3)'} + ${calc(1).negate()} | ${'calc(1 * -1)'} + `('should build $calc to get $expected', ({ calc, expected }) => { + expect(calc.build()).toBe(expected); + }); + }); + + describe('toString()', () => { + it.each` + calc | expected + ${calc(1)} | ${'1'} + ${calc(1).add(3)} | ${'1 + 3'} + ${calc(1).subtract(3)} | ${'1 - 3'} + ${calc(1).multiply(3)} | ${'1 * 3'} + ${calc(1).divide(3)} | ${'1 / 3'} + `('should convert $calc to string $expected', ({ calc, expected }) => { + expect(calc.toString()).toBe(expected); + }); + }); +}); diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts new file mode 100644 index 000000000..a55f7db70 --- /dev/null +++ b/packages/utils/src/calc/calc.ts @@ -0,0 +1,211 @@ +/** + * @file Defines a class for creating CSS calc() functions. This class allows + * for chaining operations to build up a calc() function. + * @author rowan-gud + * @author michaeltaranto + * @author markdalgleish + +/** + * An input to a calc function. Can be a number, string, or another Calc + * instance. + * + * NOTE: Unlike a lot of other places in this library, numbers will not be + * interpreted as pixel values since they can be used to represent unitless + * values. + */ +type Operand = string | number | Calc; + +class Calc { + expressions: string[]; + + private static isAddSubCalc(expr: Operand): expr is Calc { + return expr instanceof Calc && Calc.isAddSub(expr); + } + + private static isAddSub(calc: Calc): boolean { + return calc.operator === '+' || calc.operator === '-'; + } + + constructor( + exprs: Operand[], + private readonly operator?: '+' | '-' | '*' | '/' | '', + ) { + this.expressions = exprs.map((e) => e.toString()); + } + + /** + * Add operands to the current Calc instance + * + * @param operands - The operands to add + * @returns A new Calc instance with the added operands + * @example + * ```ts + * const result = calc(1).add(3); + * + * expect(result.toString()).toBe('1 + 3'); + * expect(result.build()).toBe('calc(1 + 3)'); + * ``` + */ + public add(...operands: Operand[]): Calc { + return new Calc([this.toString(), ...operands], '+'); + } + + /** + * Subtract operands from the current Calc instance + * + * @param operands - The operands to subtract + * @returns A new Calc instance with the subtracted operands + * @example + * ```ts + * const result = calc(1).subtract(3); + * + * expect(result.toString()).toBe('1 - 3'); + * expect(result.build()).toBe('calc(1 - 3)'); + * ``` + */ + public subtract(...operands: Operand[]): Calc { + return new Calc([this.toString(), ...operands], '-'); + } + + /** + * Multiply operands with the current Calc instance. If any of the operands + * are Calc instances with the operator '+' or '-', they will be wrapped in + * parentheses. + * + * @param operands - The operands to multiply + * @returns A new Calc instance with the multiplied operands + * @example + * ```ts + * const result = calc(1).multiply(3); + * + * expect(result.toString()).toBe('1 * 3'); + * expect(result.build()).toBe('calc(1 * 3)'); + * + * const result2 = calc(1).add(2, 3).multiply(4); + * + * expect(result2.toString()).toBe('(1 + 2 + 3) * 4'); + * expect(result2.build()).toBe('calc((1 + 2 + 3) * 4)'); + * ``` + */ + public multiply(...operands: Operand[]): Calc { + return new Calc( + [ + this.toString(), + ...operands.map((e) => + Calc.isAddSubCalc(e) ? `(${e.toString()})` : e, + ), + ], + '*', + ); + } + + /** + * Divide the current Calc instance by the operands. If any of the operands + * are Calc instances with the operator '+' or '-', they will be wrapped in + * parentheses. + * + * @param operands - The operands to divide by + * @returns A new Calc instance with the divided operands + * @example + * ```ts + * const result = calc(1).divide(2); + * + * expect(result.toString()).toBe('1 / 2'); + * expect(result.build()).toBe('calc(1 / 2)'); + * ``` + */ + public divide(...operands: Operand[]): Calc { + return new Calc( + [ + this.toString(), + ...operands.map((e) => + Calc.isAddSubCalc(e) ? `(${e.toString()})` : e, + ), + ], + '/', + ); + } + + /** + * Negate the current Calc instance. If the current Calc instance has the + * operator '+' or '-', it will be wrapped in parentheses. + * + * @returns A new Calc instance with the negated value + * @example + * ```ts + * const result = calc(1).negate(); + * + * expect(result.toString()).toBe('1 * -1'); + * expect(result.build()).toBe('calc(1 * -1)'); + * + * const result2 = calc(1).add(3).negate(); + * + * expect(result2.toString()).toBe('(1 + 3) * -1'); + * expect(result2.build()).toBe('calc((1 + 3) * -1)'); + * ``` + */ + public negate(): Calc { + return new Calc( + [Calc.isAddSub(this) ? `(${this.toString()})` : this.toString(), -1], + '*', + ); + } + + /** + * Build the current Calc instance into a CSS calc() function + * + * @returns The CSS calc() function + * @example + * ```ts + * const result = calc(1).add(3); + * + * expect(result.build()).toBe('calc(1 + 3)'); + * ``` + */ + public build(): string { + return `calc(${this.toString()})`; + } + + /** + * Convert the current Calc instance to a string + * + * NOTE: This is not the same as calling build(). The resulting string will + * not be wrapped in a calc() function + * + * @returns The string representation of the Calc instance + * @example + * ```ts + * const result = calc(1).add(3); + * + * expect(result.toString()).toBe('1 + 3'); + * ``` + */ + public toString(): string { + if (this.operator === undefined) { + return this.expressions[0] ?? ''; + } + + return this.expressions.join(` ${this.operator} `); + } +} + +/** + * Create a new Calc instance with the given operand + * + * @param x - The operand to create the Calc instance with + * @returns A new Calc instance + * @example + * ```ts + * const headerHeight = createVar() + * + * const root = style({ + * height: calc(headerHeight) + * .divide(2) + * .build(), + * }) + * ``` + */ +export function calc(x: Operand): Calc { + // "Ensure" that this is the only way to create a Calc instance + return new Calc([x]); +} diff --git a/packages/utils/src/calc/index.ts b/packages/utils/src/calc/index.ts new file mode 100644 index 000000000..80f9b1774 --- /dev/null +++ b/packages/utils/src/calc/index.ts @@ -0,0 +1 @@ +export * from './calc'; diff --git a/packages/utils/src/index.test.ts b/packages/utils/src/index.test.ts deleted file mode 100644 index 9de8ca419..000000000 --- a/packages/utils/src/index.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { calc } from './'; - -describe('utils', () => { - describe('calc', () => { - it('standard usage', () => { - expect(calc('10px').add('20px').toString()).toMatchInlineSnapshot( - `"calc(10px + 20px)"`, - ); - expect(calc('10px').add('20px', '30px').toString()).toMatchInlineSnapshot( - `"calc(10px + 20px + 30px)"`, - ); - expect(calc('20px').subtract('10px').toString()).toMatchInlineSnapshot( - `"calc(20px - 10px)"`, - ); - expect( - calc('20px').subtract('5px', '5px').toString(), - ).toMatchInlineSnapshot(`"calc(20px - 5px - 5px)"`); - expect(calc('10px').multiply(10).toString()).toMatchInlineSnapshot( - `"calc(10px * 10)"`, - ); - expect(calc('10px').multiply(10, 2).toString()).toMatchInlineSnapshot( - `"calc(10px * 10 * 2)"`, - ); - expect(calc('10px').divide(10).toString()).toMatchInlineSnapshot( - `"calc(10px / 10)"`, - ); - expect(calc('10px').divide(10, 2).toString()).toMatchInlineSnapshot( - `"calc(10px / 10 / 2)"`, - ); - expect( - calc('10px').add('20px').multiply(2).toString(), - ).toMatchInlineSnapshot(`"calc((10px + 20px) * 2)"`); - expect( - calc('10px').add('20px').divide(2).toString(), - ).toMatchInlineSnapshot(`"calc((10px + 20px) / 2)"`); - expect( - calc('20px').subtract('10px').negate().toString(), - ).toMatchInlineSnapshot(`"calc((20px - 10px) * -1)"`); - expect( - calc('10px').multiply(100).divide(2).negate().toString(), - ).toMatchInlineSnapshot(`"calc(((10px * 100) / 2) * -1)"`); - expect( - calc('10px') - .add('50px') - .subtract('20px') - .multiply(100) - .divide(2) - .negate() - .toString(), - ).toMatchInlineSnapshot( - `"calc(((((10px + 50px) - 20px) * 100) / 2) * -1)"`, - ); - }); - - it('bailing early', () => { - expect(calc('10px').toString()).toMatchInlineSnapshot(`"10px"`); - }); - - it('string coercion', () => { - expect(calc('10px').toString()).toMatchInlineSnapshot(`"10px"`); - expect(calc('10px').add('20px').toString()).toMatchInlineSnapshot( - `"calc(10px + 20px)"`, - ); - expect(`${calc('10px').add('20px')}`).toMatchInlineSnapshot( - `"calc(10px + 20px)"`, - ); - expect( - `${calc('10px').add(calc('20px').subtract('4em'))}`, - ).toMatchInlineSnapshot(`"calc(10px + (20px - 4em))"`); - expect(`${calc('10px').add(calc('20px'))}`).toMatchInlineSnapshot( - `"calc(10px + 20px)"`, - ); - }); - }); - - it('add', () => { - expect(calc.add(1, 2)).toMatchInlineSnapshot(`"calc(1 + 2)"`); - expect(calc.add(1, 2, 3)).toMatchInlineSnapshot(`"calc(1 + 2 + 3)"`); - expect(calc.add('1', 2, 3 - 4)).toMatchInlineSnapshot(`"calc(1 + 2 + -1)"`); - expect(calc.add('10px', '2em')).toMatchInlineSnapshot(`"calc(10px + 2em)"`); - expect( - calc.add('10px', '2em', calc.add('2', '6rem')), - ).toMatchInlineSnapshot(`"calc(10px + 2em + (2 + 6rem))"`); - expect( - calc.add( - calc.multiply( - calc.subtract('10px', '2em'), - calc.add('2', '6rem'), - '4px', - ), - ), - ).toMatchInlineSnapshot(`"calc(((10px - 2em) * (2 + 6rem) * 4px))"`); - }); - - it('subtract', () => { - expect(calc.subtract(1, 2)).toMatchInlineSnapshot(`"calc(1 - 2)"`); - expect(calc.subtract(1, 2, 3)).toMatchInlineSnapshot(`"calc(1 - 2 - 3)"`); - expect(calc.subtract('1', 2, 3 - 4)).toMatchInlineSnapshot( - `"calc(1 - 2 - -1)"`, - ); - expect(calc.subtract('10px', '2em')).toMatchInlineSnapshot( - `"calc(10px - 2em)"`, - ); - expect( - calc.subtract('10px', '2em', calc.add('2', '6rem')), - ).toMatchInlineSnapshot(`"calc(10px - 2em - (2 + 6rem))"`); - }); - - it('multiply', () => { - expect(calc.multiply(1, 2)).toMatchInlineSnapshot(`"calc(1 * 2)"`); - expect(calc.multiply(1, 2, 3)).toMatchInlineSnapshot(`"calc(1 * 2 * 3)"`); - expect(calc.multiply('1', 2, 3 - 4)).toMatchInlineSnapshot( - `"calc(1 * 2 * -1)"`, - ); - expect(calc.multiply('10px', '2em')).toMatchInlineSnapshot( - `"calc(10px * 2em)"`, - ); - expect( - calc.multiply('10px', '2em', calc.add('2', '6rem')), - ).toMatchInlineSnapshot(`"calc(10px * 2em * (2 + 6rem))"`); - }); - - it('divide', () => { - expect(calc.divide(1, 2)).toMatchInlineSnapshot(`"calc(1 / 2)"`); - expect(calc.divide(1, 2, 3)).toMatchInlineSnapshot(`"calc(1 / 2 / 3)"`); - expect(calc.divide('1', 2, 3 - 4)).toMatchInlineSnapshot( - `"calc(1 / 2 / -1)"`, - ); - expect(calc.divide('10px', '2em')).toMatchInlineSnapshot( - `"calc(10px / 2em)"`, - ); - expect( - calc.divide('10px', '2em', calc.add('2', '6rem')), - ).toMatchInlineSnapshot(`"calc(10px / 2em / (2 + 6rem))"`); - }); - - it('negate', () => { - expect(calc.negate(2)).toMatchInlineSnapshot(`"calc(2 * -1)"`); - expect(calc.negate(3 - 4)).toMatchInlineSnapshot(`"calc(-1 * -1)"`); - expect(calc.negate(calc.add('10px', '2em'))).toMatchInlineSnapshot( - `"calc((10px + 2em) * -1)"`, - ); - }); -}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 47d1f4db3..b51d8c2f7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,60 +1 @@ -type Operator = '+' | '-' | '*' | '/'; -type Operand = string | number | CalcChain; - -const toExpression = (operator: Operator, ...operands: Array) => - operands - .map((o) => `${o}`) - .join(` ${operator} `) - .replace(/calc/g, ''); - -const add = (...operands: Array) => - `calc(${toExpression('+', ...operands)})`; - -const subtract = (...operands: Array) => - `calc(${toExpression('-', ...operands)})`; - -const multiply = (...operands: Array) => - `calc(${toExpression('*', ...operands)})`; - -const divide = (...operands: Array) => - `calc(${toExpression('/', ...operands)})`; - -const negate = (x: Operand) => multiply(x, -1); - -type CalcChain = { - add: (...operands: Array) => CalcChain; - subtract: (...operands: Array) => CalcChain; - multiply: (...operands: Array) => CalcChain; - divide: (...operands: Array) => CalcChain; - negate: () => CalcChain; - toString: () => string; -}; - -interface Calc { - (x: Operand): CalcChain; - add: typeof add; - subtract: typeof subtract; - multiply: typeof multiply; - divide: typeof divide; - negate: typeof negate; -} - -export const calc: Calc = Object.assign( - (x: Operand): CalcChain => { - return { - add: (...operands) => calc(add(x, ...operands)), - subtract: (...operands) => calc(subtract(x, ...operands)), - multiply: (...operands) => calc(multiply(x, ...operands)), - divide: (...operands) => calc(divide(x, ...operands)), - negate: () => calc(negate(x)), - toString: () => x.toString(), - }; - }, - { - add, - subtract, - multiply, - divide, - negate, - }, -); +export { calc } from './calc'; From 1095b827a9c9e91d26c935cbcf0c529a5f9c9b7d Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 20:57:17 -0700 Subject: [PATCH 2/6] switch around toString and build --- packages/utils/src/calc/calc.test.ts | 18 ++++----- packages/utils/src/calc/calc.ts | 60 ++++++++++++++-------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/utils/src/calc/calc.test.ts b/packages/utils/src/calc/calc.test.ts index 9c0f11710..e3a803663 100644 --- a/packages/utils/src/calc/calc.test.ts +++ b/packages/utils/src/calc/calc.test.ts @@ -24,7 +24,7 @@ describe('class UnitCalc', () => { expressions: expectedExpressions, operator: '+', }); - expect(result.build()).toBe(expectedBuilt); + expect(result.toString()).toBe(expectedBuilt); }, ); }); @@ -50,7 +50,7 @@ describe('class UnitCalc', () => { expressions: expectedExpressions, operator: '-', }); - expect(result.build()).toBe(expectedBuilt); + expect(result.toString()).toBe(expectedBuilt); }, ); }); @@ -76,7 +76,7 @@ describe('class UnitCalc', () => { expressions: expectedExpressions, operator: '*', }); - expect(result.build()).toBe(expectedBuilt); + expect(result.toString()).toBe(expectedBuilt); }, ); }); @@ -102,7 +102,7 @@ describe('class UnitCalc', () => { expressions: expectedExpressions, operator: '/', }); - expect(result.build()).toBe(expectedBuilt); + expect(result.toString()).toBe(expectedBuilt); }, ); }); @@ -125,12 +125,12 @@ describe('class UnitCalc', () => { expressions: expectedExpressions, operator: '*', }); - expect(result.build()).toBe(expectedBuilt); + expect(result.toString()).toBe(expectedBuilt); }, ); }); - describe('build()', () => { + describe('toString()', () => { it.each` calc | expected ${calc(1)} | ${'calc(1)'} @@ -140,11 +140,11 @@ describe('class UnitCalc', () => { ${calc(1).divide(3)} | ${'calc(1 / 3)'} ${calc(1).negate()} | ${'calc(1 * -1)'} `('should build $calc to get $expected', ({ calc, expected }) => { - expect(calc.build()).toBe(expected); + expect(calc.toString()).toBe(expected); }); }); - describe('toString()', () => { + describe('build()', () => { it.each` calc | expected ${calc(1)} | ${'1'} @@ -153,7 +153,7 @@ describe('class UnitCalc', () => { ${calc(1).multiply(3)} | ${'1 * 3'} ${calc(1).divide(3)} | ${'1 / 3'} `('should convert $calc to string $expected', ({ calc, expected }) => { - expect(calc.toString()).toBe(expected); + expect(calc.build()).toBe(expected); }); }); }); diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts index a55f7db70..6df09c5ad 100644 --- a/packages/utils/src/calc/calc.ts +++ b/packages/utils/src/calc/calc.ts @@ -30,7 +30,9 @@ class Calc { exprs: Operand[], private readonly operator?: '+' | '-' | '*' | '/' | '', ) { - this.expressions = exprs.map((e) => e.toString()); + this.expressions = exprs.map((e) => + e instanceof Calc ? e.build() : e.toString(), + ); } /** @@ -42,12 +44,12 @@ class Calc { * ```ts * const result = calc(1).add(3); * - * expect(result.toString()).toBe('1 + 3'); - * expect(result.build()).toBe('calc(1 + 3)'); + * expect(result.build()).toBe('1 + 3'); + * expect(result.toString()).toBe('calc(1 + 3)'); * ``` */ public add(...operands: Operand[]): Calc { - return new Calc([this.toString(), ...operands], '+'); + return new Calc([this.build(), ...operands], '+'); } /** @@ -59,12 +61,12 @@ class Calc { * ```ts * const result = calc(1).subtract(3); * - * expect(result.toString()).toBe('1 - 3'); - * expect(result.build()).toBe('calc(1 - 3)'); + * expect(result.build()).toBe('1 - 3'); + * expect(result.toString()).toBe('calc(1 - 3)'); * ``` */ public subtract(...operands: Operand[]): Calc { - return new Calc([this.toString(), ...operands], '-'); + return new Calc([this.build(), ...operands], '-'); } /** @@ -78,22 +80,20 @@ class Calc { * ```ts * const result = calc(1).multiply(3); * - * expect(result.toString()).toBe('1 * 3'); - * expect(result.build()).toBe('calc(1 * 3)'); + * expect(result.build()).toBe('1 * 3'); + * expect(result.toString()).toBe('calc(1 * 3)'); * * const result2 = calc(1).add(2, 3).multiply(4); * - * expect(result2.toString()).toBe('(1 + 2 + 3) * 4'); - * expect(result2.build()).toBe('calc((1 + 2 + 3) * 4)'); + * expect(result2.build()).toBe('(1 + 2 + 3) * 4'); + * expect(result2.toString()).toBe('calc((1 + 2 + 3) * 4)'); * ``` */ public multiply(...operands: Operand[]): Calc { return new Calc( [ - this.toString(), - ...operands.map((e) => - Calc.isAddSubCalc(e) ? `(${e.toString()})` : e, - ), + this.build(), + ...operands.map((e) => (Calc.isAddSubCalc(e) ? `(${e.build()})` : e)), ], '*', ); @@ -110,17 +110,15 @@ class Calc { * ```ts * const result = calc(1).divide(2); * - * expect(result.toString()).toBe('1 / 2'); - * expect(result.build()).toBe('calc(1 / 2)'); + * expect(result.build()).toBe('1 / 2'); + * expect(result.toString()).toBe('calc(1 / 2)'); * ``` */ public divide(...operands: Operand[]): Calc { return new Calc( [ - this.toString(), - ...operands.map((e) => - Calc.isAddSubCalc(e) ? `(${e.toString()})` : e, - ), + this.build(), + ...operands.map((e) => (Calc.isAddSubCalc(e) ? `(${e.build()})` : e)), ], '/', ); @@ -135,18 +133,18 @@ class Calc { * ```ts * const result = calc(1).negate(); * - * expect(result.toString()).toBe('1 * -1'); - * expect(result.build()).toBe('calc(1 * -1)'); + * expect(result.build()).toBe('1 * -1'); + * expect(result.toString()).toBe('calc(1 * -1)'); * * const result2 = calc(1).add(3).negate(); * - * expect(result2.toString()).toBe('(1 + 3) * -1'); - * expect(result2.build()).toBe('calc((1 + 3) * -1)'); + * expect(result2.build()).toBe('(1 + 3) * -1'); + * expect(result2.toString()).toBe('calc((1 + 3) * -1)'); * ``` */ public negate(): Calc { return new Calc( - [Calc.isAddSub(this) ? `(${this.toString()})` : this.toString(), -1], + [Calc.isAddSub(this) ? `(${this.build()})` : this.build(), -1], '*', ); } @@ -159,11 +157,11 @@ class Calc { * ```ts * const result = calc(1).add(3); * - * expect(result.build()).toBe('calc(1 + 3)'); + * expect(result.toString()).toBe('calc(1 + 3)'); * ``` */ - public build(): string { - return `calc(${this.toString()})`; + public toString(): string { + return `calc(${this.build()})`; } /** @@ -177,10 +175,10 @@ class Calc { * ```ts * const result = calc(1).add(3); * - * expect(result.toString()).toBe('1 + 3'); + * expect(result.build()).toBe('1 + 3'); * ``` */ - public toString(): string { + public build(): string { if (this.operator === undefined) { return this.expressions[0] ?? ''; } From fbaafbb9e079755a8e6b5f572361c92bd1365341 Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 20:57:53 -0700 Subject: [PATCH 3/6] remove old comment --- packages/utils/src/calc/calc.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts index 6df09c5ad..5756b7bf3 100644 --- a/packages/utils/src/calc/calc.ts +++ b/packages/utils/src/calc/calc.ts @@ -204,6 +204,5 @@ class Calc { * ``` */ export function calc(x: Operand): Calc { - // "Ensure" that this is the only way to create a Calc instance return new Calc([x]); } From cdf963094995dc6c6856046000cc5da08f6171c1 Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 21:15:50 -0700 Subject: [PATCH 4/6] do all the add / sub logic in one place --- packages/utils/src/calc/calc.ts | 53 +++++++++++++-------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts index 5756b7bf3..aa125b516 100644 --- a/packages/utils/src/calc/calc.ts +++ b/packages/utils/src/calc/calc.ts @@ -18,21 +18,25 @@ type Operand = string | number | Calc; class Calc { expressions: string[]; - private static isAddSubCalc(expr: Operand): expr is Calc { - return expr instanceof Calc && Calc.isAddSub(expr); - } - - private static isAddSub(calc: Calc): boolean { - return calc.operator === '+' || calc.operator === '-'; - } - constructor( exprs: Operand[], - private readonly operator?: '+' | '-' | '*' | '/' | '', + private readonly operator?: '+' | '-' | '*' | '/', ) { - this.expressions = exprs.map((e) => - e instanceof Calc ? e.build() : e.toString(), - ); + const isMultiplyDivide = operator === '*' || operator === '/'; + + this.expressions = exprs.map((e) => { + if (e instanceof Calc) { + const childIsAddSub = e.operator === '+' || e.operator === '-'; + + if (isMultiplyDivide && childIsAddSub) { + return `(${e.build()})`; + } + + return e.build(); + } + + return e.toString(); + }); } /** @@ -49,7 +53,7 @@ class Calc { * ``` */ public add(...operands: Operand[]): Calc { - return new Calc([this.build(), ...operands], '+'); + return new Calc([this, ...operands], '+'); } /** @@ -66,7 +70,7 @@ class Calc { * ``` */ public subtract(...operands: Operand[]): Calc { - return new Calc([this.build(), ...operands], '-'); + return new Calc([this, ...operands], '-'); } /** @@ -90,13 +94,7 @@ class Calc { * ``` */ public multiply(...operands: Operand[]): Calc { - return new Calc( - [ - this.build(), - ...operands.map((e) => (Calc.isAddSubCalc(e) ? `(${e.build()})` : e)), - ], - '*', - ); + return new Calc([this, ...operands], '*'); } /** @@ -115,13 +113,7 @@ class Calc { * ``` */ public divide(...operands: Operand[]): Calc { - return new Calc( - [ - this.build(), - ...operands.map((e) => (Calc.isAddSubCalc(e) ? `(${e.build()})` : e)), - ], - '/', - ); + return new Calc([this, ...operands], '/'); } /** @@ -143,10 +135,7 @@ class Calc { * ``` */ public negate(): Calc { - return new Calc( - [Calc.isAddSub(this) ? `(${this.build()})` : this.build(), -1], - '*', - ); + return new Calc([this, -1], '*'); } /** From 3fb26266aa7aa386da771b7b250207a6b1ade18d Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 21:21:58 -0700 Subject: [PATCH 5/6] switch negate to front because looks nicer --- packages/utils/src/calc/calc.test.ts | 14 +++++++------- packages/utils/src/calc/calc.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/utils/src/calc/calc.test.ts b/packages/utils/src/calc/calc.test.ts index e3a803663..8c2a66ea3 100644 --- a/packages/utils/src/calc/calc.test.ts +++ b/packages/utils/src/calc/calc.test.ts @@ -110,12 +110,12 @@ describe('class UnitCalc', () => { describe('negate()', () => { it.each` calc | expectedExpressions | expectedBuilt - ${calc(1)} | ${['1', '-1']} | ${'calc(1 * -1)'} - ${calc(1).add(3)} | ${['(1 + 3)', '-1']} | ${'calc((1 + 3) * -1)'} - ${calc(1).subtract(3)} | ${['(1 - 3)', '-1']} | ${'calc((1 - 3) * -1)'} - ${calc(1).multiply(3)} | ${['1 * 3', '-1']} | ${'calc(1 * 3 * -1)'} - ${calc(1).divide(3)} | ${['1 / 3', '-1']} | ${'calc(1 / 3 * -1)'} - ${calc(css)} | ${[css, '-1']} | ${'calc(var(--some-css-var) * -1)'} + ${calc(1)} | ${['-1', '1']} | ${'calc(-1 * 1)'} + ${calc(1).add(3)} | ${['-1', '(1 + 3)']} | ${'calc(-1 * (1 + 3))'} + ${calc(1).subtract(3)} | ${['-1', '(1 - 3)']} | ${'calc(-1 * (1 - 3))'} + ${calc(1).multiply(3)} | ${['-1', '1 * 3']} | ${'calc(-1 * 1 * 3)'} + ${calc(1).divide(3)} | ${['-1', '1 / 3']} | ${'calc(-1 * 1 / 3)'} + ${calc(css)} | ${['-1', css]} | ${'calc(-1 * var(--some-css-var))'} `( 'should negate $calc to get $expectedBuilt', ({ calc, expectedExpressions, expectedBuilt }) => { @@ -138,7 +138,7 @@ describe('class UnitCalc', () => { ${calc(1).subtract(3)} | ${'calc(1 - 3)'} ${calc(1).multiply(3)} | ${'calc(1 * 3)'} ${calc(1).divide(3)} | ${'calc(1 / 3)'} - ${calc(1).negate()} | ${'calc(1 * -1)'} + ${calc(1).negate()} | ${'calc(-1 * 1)'} `('should build $calc to get $expected', ({ calc, expected }) => { expect(calc.toString()).toBe(expected); }); diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts index aa125b516..4cc934b9f 100644 --- a/packages/utils/src/calc/calc.ts +++ b/packages/utils/src/calc/calc.ts @@ -135,7 +135,7 @@ class Calc { * ``` */ public negate(): Calc { - return new Calc([this, -1], '*'); + return new Calc([-1, this], '*'); } /** From fdadf68942b6e0f859450d61b5c3d883f6a3a88c Mon Sep 17 00:00:00 2001 From: Rowan Gudmundsson Date: Tue, 4 Jun 2024 21:56:24 -0700 Subject: [PATCH 6/6] words are hard --- packages/utils/src/calc/calc.ts | 29 ++++++++++++++++++++--------- packages/utils/src/index.ts | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/utils/src/calc/calc.ts b/packages/utils/src/calc/calc.ts index 4cc934b9f..b3b5516cd 100644 --- a/packages/utils/src/calc/calc.ts +++ b/packages/utils/src/calc/calc.ts @@ -16,22 +16,33 @@ type Operand = string | number | Calc; class Calc { - expressions: string[]; + private static isTerm(op: Operand): op is Calc { + return op instanceof Calc && (op.operator === '+' || op.operator === '-'); + } + + private static isFactor(op: Operand): op is Calc { + return op instanceof Calc && (op.operator === '*' || op.operator === '/'); + } + + /** + * The expressions to combine with the operator + */ + private readonly expressions: string[]; constructor( exprs: Operand[], + /** + * The operator to use when combining the expressions. If undefined, the + * Calc instance only has one expression. + */ private readonly operator?: '+' | '-' | '*' | '/', ) { - const isMultiplyDivide = operator === '*' || operator === '/'; + const thisIsFactor = Calc.isFactor(this); this.expressions = exprs.map((e) => { - if (e instanceof Calc) { - const childIsAddSub = e.operator === '+' || e.operator === '-'; - - if (isMultiplyDivide && childIsAddSub) { - return `(${e.build()})`; - } - + if (thisIsFactor && Calc.isTerm(e)) { + return `(${e.build()})`; + } else if (e instanceof Calc) { return e.build(); } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b51d8c2f7..6fb10b9b7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,2 @@ export { calc } from './calc'; +export { transition } from './transition';