From dad7e9ce72678d086d64e19baaca8152164e0437 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 31 Oct 2023 22:42:01 -0500 Subject: [PATCH] feat: adds ceil, floor, and round functions Includes tests, version bump, and update entry --- public/dimensional_analysis.py | 18 +++++- src/App.svelte | 2 +- src/Updates.svelte | 15 +++++ src/keyboard/Keyboard.ts | 12 ++-- src/parser/constants.ts | 5 +- tests/test_functions.spec.mjs | 108 +++++++++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 10 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 911c77d1..20bcbd9d 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -56,7 +56,9 @@ Subs, Pow, MatMul, - Eq + Eq, + floor, + ceiling ) class ExprWithAssumptions(Expr): @@ -681,6 +683,12 @@ def ensure_unitless_in_angle_out(arg): else: raise TypeError('Unitless input argument required for function') +def ensure_unitless_in(arg): + if dimsys_SI.get_dimensional_dependencies(arg) == {}: + return arg + else: + raise TypeError('Unitless input argument required for function') + def ensure_any_unit_in_angle_out(arg): # ensure input arg units make sense (will raise if inconsistent) dimsys_SI.get_dimensional_dependencies(arg) @@ -733,6 +741,9 @@ def custom_matmul(exp1: Expr, exp2: Expr): else: return MatMul(exp1, exp2) +def custom_round(expression: Expr): + return expression.round() + class PlaceholderFunction(TypedDict): dim_func: Callable sympy_func: object @@ -780,7 +791,10 @@ def custom_dot(exp1: Matrix, exp2: Matrix): Function('_IndexMatrix') : {"dim_func": IndexMatrix, "sympy_func": IndexMatrix}, Function('_Eq') : {"dim_func": Eq, "sympy_func": Eq}, Function('_norm') : {"dim_func": custom_norm, "sympy_func": custom_norm}, - Function('_dot') : {"dim_func": custom_dot, "sympy_func": custom_dot} + Function('_dot') : {"dim_func": custom_dot, "sympy_func": custom_dot}, + Function('_ceil') : {"dim_func": ensure_unitless_in, "sympy_func": ceiling}, + Function('_floor') : {"dim_func": ensure_unitless_in, "sympy_func": floor}, + Function('_round') : {"dim_func": ensure_unitless_in, "sympy_func": custom_round}, } placeholder_set = set(placeholder_map.keys()) diff --git a/src/App.svelte b/src/App.svelte index 8139f7fc..861a3952 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -84,7 +84,7 @@ const apiUrl = window.location.origin; - const currentVersion = 20230930; + const currentVersion = 20231031; const tutorialHash = "fFjTsnFoSQMLwcvteVoNtL"; const termsVersion = 20230608; diff --git a/src/Updates.svelte b/src/Updates.svelte index 307faefd..39bbf7be 100644 --- a/src/Updates.svelte +++ b/src/Updates.svelte @@ -16,6 +16,21 @@ import { modifierKey } from "./stores"; } +October 31, 2023 +

New ceil, floor, and round Functions Have Been Added

+

+ Three new rounding functions have been added. ceil to round up, + floor to round down, and round to round to the nearest integer. + These new functions can be found on the f(x) tab of the virtual keyboard. + Note that these functions require that the input parameter is unitless. An input + with units would lead to unexpected results since all calculation are performed + internally in base SI units. For example, calculating the floor of + 1 [mm] would evaluate to zero since the floor would be calculated on 0.001 after + the millimeter value is converted to meters for the internal calculation. +

+ +
+ September 30, 2023

Improved Error Messages

diff --git a/src/keyboard/Keyboard.ts b/src/keyboard/Keyboard.ts index 7d92eada..7593ea62 100644 --- a/src/keyboard/Keyboard.ts +++ b/src/keyboard/Keyboard.ts @@ -494,8 +494,8 @@ export const keyboards: Keyboards = { new Button({ buttonText: '\\mathrm{real}', content: '\\mathrm{real}\\left(#0\\right)', command: "insert", size: '1.2fr' }), new Button({ buttonText: '\\left|x\\right|', content: '\\left|#0\\right|', command: "insert" }), new Blank('0.1fr'), - new Blank('1fr'), - new Button({ buttonText: '⌫', command: 'deleteBackward' }), + new Button({ buttonText: '\\mathrm{ceil}', content: '\\mathrm{ceil}\\left(#0\\right)', command: "insert"}), + new Button({ buttonText: '⌫', command: 'deleteBackward', size: '1.2fr'}), ], [ new Button({ buttonText: '\\cos', content: '\\cos\\left(#0\\right)', command: "insert" }), @@ -506,8 +506,8 @@ export const keyboards: Keyboards = { new Button({ buttonText: '\\mathrm{imag}', content: '\\mathrm{imag}\\left(#0\\right)', command: "insert", size: '1.2fr' }), new Button({ buttonText: '\\mathrm{max}', content: '\\mathrm{max}\\left(#0\\right)', command: "insert" }), new Blank('0.1fr'), - new Button({ buttonText: '\\leftarrow', command: 'moveToPreviousChar' }), - new Button({ buttonText: '\\rightarrow', command: 'moveToNextChar' }), + new Button({ buttonText: '\\mathrm{floor}', content: '\\mathrm{floor}\\left(#0\\right)', command: "insert"}), + new Button({ buttonText: '\\mathrm{round}', content: '\\mathrm{round}\\left(#0\\right)', command: "insert", size: '1.2fr'}), ], [ new Button({ buttonText: '\\tan', content: '\\tan\\left(#0\\right)', command: "insert" }), @@ -519,7 +519,7 @@ export const keyboards: Keyboards = { new Button({ buttonText: '\\mathrm{min}', content: '\\mathrm{min}\\left(#0\\right)', command: "insert" }), new Blank('0.1fr'), new Button({ buttonText: "\\int", content: '\\int \\left(#0\\right)\\mathrm{d}\\left(#?\\right)', command: "insert", fontSize: '10pt' }), - new Button({ buttonText: '\\int_a^b', content: '\\int _{#?}^{#?}\\left(#0\\right)\\mathrm{d}\\left(#?\\right)', command: "insert", fontSize: '10pt' }), + new Button({ buttonText: '\\int_a^b', content: '\\int _{#?}^{#?}\\left(#0\\right)\\mathrm{d}\\left(#?\\right)', command: "insert", fontSize: '10pt', size: '1.2fr'}), ], [ new Button({ buttonText: '\\ln', content: '\\ln\\left(#0\\right)', command: "insert" }), @@ -531,7 +531,7 @@ export const keyboards: Keyboards = { new Blank(), new Blank('0.1fr'), new Button({ buttonText: "x^{\\prime}", content: '\\frac{\\mathrm{d}}{\\mathrm{d}\\left(#?\\right)}\\left(#0\\right)', command: "insert", fontSize: '12pt' }), - new Button({ buttonText: "x^{\\prime \\prime}", content: '\\frac{\\mathrm{d}^{2}}{\\mathrm{d}\\left(#?\\right)^{2}}\\left(#0\\right)', command: "insert", fontSize: '12pt' }) + new Button({ buttonText: "x^{\\prime \\prime}", content: '\\frac{\\mathrm{d}^{2}}{\\mathrm{d}\\left(#?\\right)^{2}}\\left(#0\\right)', command: "insert", fontSize: '12pt', size: '1.2fr'}) ]] } }, diff --git a/src/parser/constants.ts b/src/parser/constants.ts index 7902a170..f782fbeb 100644 --- a/src/parser/constants.ts +++ b/src/parser/constants.ts @@ -17,7 +17,10 @@ export const BUILTIN_FUNCTION_MAP = new Map([ ['det', '_Determinant'], ['transpose', '_Transpose'], ['norm', '_norm'], - ['dot', '_dot'] + ['dot', '_dot'], + ['floor', '_floor'], + ['ceil', '_ceil'], + ['round', '_round'] ]); export const COMPARISON_MAP = new Map([ diff --git a/tests/test_functions.spec.mjs b/tests/test_functions.spec.mjs index ec8e3dc2..1b1f0ac4 100644 --- a/tests/test_functions.spec.mjs +++ b/tests/test_functions.spec.mjs @@ -203,4 +203,112 @@ test('Test min/max functions', async ({ browserName }) => { content = await page.locator('#result-units-12').textContent(); expect(content).toBe('N'); +}); + + +test('Test ceil func', async ({ browserName }) => { + + await page.locator('#cell-0 >> math-field.editable').type('ceil(1.1)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-1 >> math-field.editable').type('ceil(1.6)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-2 >> math-field.editable').type('ceil(2.00000)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-3 >> math-field.editable').type('ceil(-2.6)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-4 >> math-field.editable').type('ceil(1[m])='); + + await page.waitForSelector('.status-footer', { state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(content).toBe('2'); + + content = await page.textContent('#result-value-1'); + expect(content).toBe('2'); + + content = await page.textContent('#result-value-2'); + expect(content).toBe('2'); + + content = await page.textContent('#result-value-3'); + expect(content).toBe('-2'); + + await expect(page.locator('#cell-4 >> text=Dimension Error')).toBeAttached(); +}); + + +test('Test floor func', async ({ browserName }) => { + + await page.locator('#cell-0 >> math-field.editable').type('floor(1.1)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-1 >> math-field.editable').type('floor(1.6)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-2 >> math-field.editable').type('floor(1.00000)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-3 >> math-field.editable').type('floor(-0.9)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-4 >> math-field.editable').type('floor(1[m])='); + + await page.waitForSelector('.status-footer', { state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(content).toBe('1'); + + content = await page.textContent('#result-value-1'); + expect(content).toBe('1'); + + content = await page.textContent('#result-value-2'); + expect(content).toBe('1'); + + content = await page.textContent('#result-value-3'); + expect(content).toBe('-1'); + + await expect(page.locator('#cell-4 >> text=Dimension Error')).toBeAttached(); +}); + + +test('Test round func', async ({ browserName }) => { + + await page.locator('#cell-0 >> math-field.editable').type('round(1.1)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-1 >> math-field.editable').type('round(1.6)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-2 >> math-field.editable').type('round(1.00000)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-3 >> math-field.editable').type('round(-0.9)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-4 >> math-field.editable').type('round(-1.1)='); + + await page.locator('#add-math-cell').click(); + await page.locator('#cell-5 >> math-field.editable').type('round(1[m])='); + + await page.waitForSelector('.status-footer', { state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(content).toBe('1'); + + content = await page.textContent('#result-value-1'); + expect(content).toBe('2'); + + content = await page.textContent('#result-value-2'); + expect(content).toBe('1'); + + content = await page.textContent('#result-value-3'); + expect(content).toBe('-1'); + + content = await page.textContent('#result-value-4'); + expect(content).toBe('-1'); + + await expect(page.locator('#cell-5 >> text=Dimension Error')).toBeAttached(); }); \ No newline at end of file