Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/renderer/composables/__tests__/useMathEngine.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable test/prefer-lowercase-title */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { preprocessMathExpression } from '../math-notebook/math-engine/preprocess'
import { useMathEngine } from '../math-notebook/useMathEngine'

const TEST_CURRENCY_RATES = {
Expand Down Expand Up @@ -334,6 +333,14 @@ describe('currency', () => {
expect(result.error).toBe('Currency rates service unavailable')
})

it('does not treat pound weight as currency while loading', () => {
setCurrencyServiceState('loading')

const result = evalLine('1 pound to lb')
expect(result.type).toBe('unit')
expect(result.value).toContain('lb')
})

it('$ symbol', () => {
const result = evalLine('$30')
expect(result.type).toBe('unit')
Expand Down Expand Up @@ -385,6 +392,24 @@ describe('currency', () => {
})
})

describe('modifier compatibility', () => {
it('returns controlled error for timezone format modifier', () => {
const result = evalLine('time in Paris in hex')
expect(result.type).toBe('empty')
expect(result.error).toBe(
'Modifier is not supported for this expression type',
)
})

it('returns controlled error for css rounding modifier', () => {
const result = evalLine('12 pt in px rounded')
expect(result.type).toBe('empty')
expect(result.error).toBe(
'Modifier is not supported for this expression type',
)
})
})

describe('unit conversion', () => {
it('stacked units', () => {
const result = evalLine('1 meter 20 cm')
Expand Down Expand Up @@ -963,10 +988,6 @@ describe('mixed currency and plain number', () => {
expect(result.type).toBe('unit')
})

it('does not infer currency for percentage operands', () => {
expect(preprocessMathExpression('$100 + 10%')).toBe('100 USD + 10 / 100')
})

it('keeps trailing conversion on the whole inferred expression', () => {
const result = evalLine('10 USD + 1 in RUB')
expect(result.type).toBe('unit')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'
import { analysisNormalize } from '../pipeline/analysisNormalize'

describe('analysisNormalize', () => {
it('plain expression', () => {
const view = analysisNormalize('10 + 5')
expect(view.raw).toBe('10 + 5')
expect(view.expression).toBe('10 + 5')
expect(view.normalized).toBe('10 + 5')
expect(view.label).toBeUndefined()
})

it('strips label prefix', () => {
const view = analysisNormalize('Price: $100 + $50')
expect(view.expression).toBe('$100 + $50')
expect(view.label).toBe('Price')
})

it('strips multi-word label', () => {
const view = analysisNormalize('Monthly cost: 1200 / 12')
expect(view.expression).toBe('1200 / 12')
expect(view.label).toBe('Monthly cost')
})

it('strips quoted text', () => {
const view = analysisNormalize('$275 for the "Model 227"')
expect(view.expression).toBe('$275')
})

it('strips label and quoted text', () => {
const view = analysisNormalize('Item: $99 "discount"')
expect(view.expression).toBe('$99')
expect(view.label).toBe('Item')
})

it('empty string', () => {
const view = analysisNormalize('')
expect(view.raw).toBe('')
expect(view.expression).toBe('')
expect(view.normalized).toBe('')
})

it('comment', () => {
const view = analysisNormalize('// comment')
expect(view.expression).toBe('// comment')
})

it('normalized is lowercase', () => {
const view = analysisNormalize('5 USD to EUR')
expect(view.normalized).toBe('5 usd to eur')
})

it('trims whitespace', () => {
const view = analysisNormalize(' 10 + 5 ')
expect(view.raw).toBe('10 + 5')
expect(view.expression).toBe('10 + 5')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { describe, expect, it } from 'vitest'
import { analysisNormalize } from '../pipeline/analysisNormalize'
import { classify } from '../pipeline/classify'

function classifyRaw(raw: string) {
return classify(analysisNormalize(raw))
}

describe('classify — primary intent', () => {
it('empty', () => expect(classifyRaw('').primary).toBe('empty'))
it('whitespace', () => expect(classifyRaw(' ').primary).toBe('empty'))

it('// comment', () =>
expect(classifyRaw('// text').primary).toBe('comment'))
it('# heading', () =>
expect(classifyRaw('# heading').primary).toBe('comment'))

it('sum', () => expect(classifyRaw('sum').primary).toBe('aggregate-block'))
it('total', () =>
expect(classifyRaw('total').primary).toBe('aggregate-block'))
it('average', () =>
expect(classifyRaw('average').primary).toBe('aggregate-block'))
it('avg', () => expect(classifyRaw('avg').primary).toBe('aggregate-block'))
it('median', () =>
expect(classifyRaw('median').primary).toBe('aggregate-block'))
it('count', () =>
expect(classifyRaw('count').primary).toBe('aggregate-block'))
it('sUM case insensitive', () =>
expect(classifyRaw('SUM').primary).toBe('aggregate-block'))

it('total of list', () =>
expect(classifyRaw('total of 3, 4 and 9').primary).toBe(
'aggregate-inline',
))
it('average of list', () =>
expect(classifyRaw('average of 10, 20').primary).toBe('aggregate-inline'))

it('days since', () =>
expect(classifyRaw('days since January 1').primary).toBe('calendar'))
it('days till', () =>
expect(classifyRaw('days till December 25').primary).toBe('calendar'))
it('days between', () =>
expect(classifyRaw('days between March 1 and March 31').primary).toBe(
'calendar',
))
it('5 days from now', () =>
expect(classifyRaw('5 days from now').primary).toBe('calendar'))
it('3 days ago', () =>
expect(classifyRaw('3 days ago').primary).toBe('calendar'))
it('day of the week', () =>
expect(classifyRaw('day of the week on Jan 1, 2024').primary).toBe(
'calendar',
))
it('week of year', () =>
expect(classifyRaw('week of year').primary).toBe('calendar'))
it('days in February', () =>
expect(classifyRaw('days in February 2020').primary).toBe('calendar'))
it('days in Q3', () =>
expect(classifyRaw('days in Q3').primary).toBe('calendar'))
it('current timestamp', () =>
expect(classifyRaw('current timestamp').primary).toBe('calendar'))

it('time in Paris', () => {
const c = classifyRaw('time in Paris')
expect(c.primary).toBe('timezone')
expect(c.timezoneOperation).toBe('display')
})
it('now', () => {
const c = classifyRaw('now')
expect(c.primary).toBe('timezone')
expect(c.timezoneOperation).toBe('display')
})
it('pST time', () => {
const c = classifyRaw('PST time')
expect(c.primary).toBe('timezone')
expect(c.timezoneOperation).toBe('display')
})

it('pST time - Berlin time', () => {
const c = classifyRaw('PST time - Berlin time')
expect(c.primary).toBe('timezone')
expect(c.timezoneOperation).toBe('difference')
})

it('2:30 pm HKT in Berlin', () => {
const c = classifyRaw('2:30 pm HKT in Berlin')
expect(c.primary).toBe('timezone')
expect(c.timezoneOperation).toBe('display')
})

it('ppi = 326', () => {
const c = classifyRaw('ppi = 326')
expect(c.primary).toBe('assignment')
expect(c.assignmentTarget).toBe('css')
})
it('em = 20px', () => {
const c = classifyRaw('em = 20px')
expect(c.primary).toBe('assignment')
expect(c.assignmentTarget).toBe('css')
})

it('12 pt in px', () =>
expect(classifyRaw('12 pt in px').primary).toBe('css'))
it('12 pt into px', () =>
expect(classifyRaw('12 pt into px').primary).toBe('css'))

it('x = 10', () => {
const c = classifyRaw('x = 10')
expect(c.primary).toBe('assignment')
expect(c.assignmentTarget).toBe('math')
})
it('start = today', () => {
const c = classifyRaw('start = today')
expect(c.primary).toBe('assignment')
expect(c.assignmentTarget).toBe('date')
})

it('today + 3 days', () =>
expect(classifyRaw('today + 3 days').primary).toBe('date-arithmetic'))

it('10 + 5', () => expect(classifyRaw('10 + 5').primary).toBe('math'))
it('sqrt(16)', () => expect(classifyRaw('sqrt(16)').primary).toBe('math'))
it('100 USD to EUR', () =>
expect(classifyRaw('100 USD to EUR').primary).toBe('math'))
})

describe('classify — modifiers', () => {
it('rounding: to 2 dp', () => {
const c = classifyRaw('1/3 to 2 dp')
expect(c.modifiers.rounding).toEqual({ type: 'dp', param: 2 })
})
it('rounding: rounded', () => {
const c = classifyRaw('5.5 rounded')
expect(c.modifiers.rounding).toEqual({ type: 'round', param: 0 })
})
it('rounding: to nearest 10', () => {
const c = classifyRaw('37 to nearest 10')
expect(c.modifiers.rounding).toEqual({ type: 'nearest', param: 10 })
})

it('format: in hex', () => {
const c = classifyRaw('255 in hex')
expect(c.modifiers.resultFormat).toBe('hex')
})
it('format: in sci', () => {
const c = classifyRaw('5300 in sci')
expect(c.modifiers.resultFormat).toBe('sci')
})

it('strip: as number', () => {
const c = classifyRaw('$100 as number')
expect(c.modifiers.stripUnit).toBe('number')
})
it('strip: as dec', () => {
const c = classifyRaw('20% as dec')
expect(c.modifiers.stripUnit).toBe('dec')
})
it('strip: as fraction', () => {
const c = classifyRaw('0.5 as fraction')
expect(c.modifiers.stripUnit).toBe('fraction')
})
})

describe('classify — features', () => {
it('hasCurrency for $ symbol', () => {
expect(classifyRaw('$100 + $50').features.hasCurrency).toBe(true)
})
it('hasCurrency for USD code', () => {
expect(classifyRaw('100 USD').features.hasCurrency).toBe(true)
})
it('does not treat pound weight as currency', () => {
expect(classifyRaw('1 pound to lb').features.hasCurrency).toBe(false)
})
it('no currency for plain math', () => {
expect(classifyRaw('10 + 5').features.hasCurrency).toBe(false)
})
it('hasAssignment', () => {
expect(classifyRaw('x = 10').features.hasAssignment).toBe(true)
})
it('no assignment for ==', () => {
expect(classifyRaw('5 == 5').features.hasAssignment).toBe(false)
})
it('hasConversion', () => {
expect(classifyRaw('5 km to mile').features.hasConversion).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest'
import { analysisNormalize } from '../pipeline/analysisNormalize'
import { classify } from '../pipeline/classify'
import { rewrite } from '../pipeline/rewrite'

function rewriteRaw(raw: string): string {
const view = analysisNormalize(raw)
const classification = classify(view)
return rewrite(view, classification)
}

describe('rewrite canonical output', () => {
const cases = [
['10 + 5', '10 + 5'],
['2 * 3', '2 * 3'],
['5 300', '5300'],
['45°', '45 deg'],
['2k', '(2 * 1000)'],
['3M', '(3 * 1000000)'],
['1.5 billion', '(1.5 * 1000000000)'],
['$100', '100 USD'],
['€50', '50 EUR'],
['10 dollars', '10 USD'],
['8 times 9', '8 * 9'],
['10 plus 5', '10 + 5'],
['3 multiplied by 4', '3 * 4'],
['100 divide by 4', '100 / 4'],
['17 mod 5', '17 % 5'],
['1 nautical mile', '1 nauticalmile'],
['3 days', '3 mcday'],
['2 hours', '2 mchour'],
['1 meter 20 cm', '(1 meter + 20 cm)'],
['20 sq cm', '20 cm^2'],
['cbm', 'm^3'],
['15% of 200', '15 / 100 * 200'],
['200 + 10%', '200 * (1 + 10 / 100)'],
['5% on 200', '200 * (1 + 5 / 100)'],
['50 as a % of 100', '(50 / 100) * 100'],
['5 km as mile', '5 km to mile'],
['100 celsius into fahrenheit', '100 celsius to fahrenheit'],
['sqrt 16', 'sqrt(16)'],
['log 2 (8)', 'log(8, 2)'],
['square root of 81', 'sqrt(81)'],
['log 20 base 4', 'log(20, 4)'],
['if 5 > 3 then 10 else 20', '(5 > 3) ? (10) : (20)'],
['42 if 5 > 3', '(5 > 3) ? (42) : 0'],
['Price: $100 + $50', '100 USD + 50 USD'],
['$275 for the "Model 227"', '275 USD'],
['meters in 10 km', '10 km to meters'],
['km m', '1 km to m'],
['$50 per week', '50 USD / mcweek'],
['50 to 75 is what x', '75 / 50 to multiplier'],
['6(3)', '6 * (3)'],
['50 to 75 is what %', '((75 - 50) / 50) * 100'],
['0.35 as %', '0.35 * 100'],
['$100 + 10', '100 USD + 10 USD'],
['$100 + 10%', '100 USD + 10 / 100'],
['10 USD + 1 in RUB', '(10 USD + 1 USD) to RUB'],
['3 to the power of 2', '3 ^ 2'],
['remainder of 21 divided by 5', '21 % 5'],
['2/3 of 600', '(2 / 3) * 600'],
] as const

for (const [input, expected] of cases) {
it(`"${input}"`, () => {
expect(rewriteRaw(input)).toBe(expected)
})
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,6 @@ export const knownUnitTokens = new Set([
'pixel',
'pixels',
'pt',
'point',
'points',
'em',
'mph',
'kmh',
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/composables/math-notebook/math-engine/css.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CssContext, SpecialLineResult } from './types'
import { lengthInchesByUnit } from './constants'
import { splitByKeyword } from './preprocess'
import { splitByKeyword } from './utils'

function normalizeCssUnit(unit: string) {
const normalized = unit.toLowerCase()
Expand Down
Loading