Skip to content

Commit 3126952

Browse files
ix(math): use numeric totals and infer additive units in mixed expressions (#711)
* fix(math): use numeric values for totals * feat(math): infer additive units in mixed expressions
1 parent 9ed22de commit 3126952

File tree

7 files changed

+382
-13
lines changed

7 files changed

+382
-13
lines changed

src/renderer/components/math-notebook/ResultsPanel.vue

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCopyToClipboard } from '@/composables'
55
import { formatMathNumber } from '@/composables/math-notebook/math-engine/format'
66
import { i18n, ipc } from '@/electron'
77
import { LoaderCircle, Sigma } from 'lucide-vue-next'
8+
import { sumNumericResults } from './sumNumericResults'
89
910
interface Props {
1011
results: LineResult[]
@@ -22,17 +23,7 @@ const MATH_NOTEBOOK_DOCUMENTATION_URL
2223
= 'https://masscode.io/documentation/math-notebook.html'
2324
2425
const total = computed(() => {
25-
return props.results.reduce((sum, r) => {
26-
if (r.type === 'number' || r.type === 'assignment') {
27-
const raw = r.value || ''
28-
if (raw.includes(':'))
29-
return sum
30-
const num = Number.parseFloat(raw.replace(/[^\d.\-e+]/gi, ''))
31-
if (!Number.isNaN(num))
32-
return sum + num
33-
}
34-
return sum
35-
}, 0)
26+
return sumNumericResults(props.results)
3627
})
3728
3829
const formattedTotal = computed(() => {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { LineResult } from '@/composables/math-notebook'
2+
import { describe, expect, it } from 'vitest'
3+
import { sumNumericResults } from '../sumNumericResults'
4+
5+
describe('sumNumericResults', () => {
6+
it('sums numericValue and ignores other result types', () => {
7+
const results: LineResult[] = [
8+
{ value: '10', error: null, type: 'number', numericValue: 10 },
9+
{ value: '12.03.2025, 0:00:00', error: null, type: 'assignment' },
10+
{ value: '100 USD', error: null, type: 'assignment' },
11+
{ value: '0xFF', error: null, type: 'number', numericValue: 255 },
12+
{ value: '5.3e+3', error: null, type: 'number', numericValue: 5300 },
13+
]
14+
15+
expect(sumNumericResults(results)).toBe(5565)
16+
})
17+
18+
it('ignores Infinity and NaN when numericValue is absent', () => {
19+
const results: LineResult[] = [
20+
{ value: 'Infinity', error: null, type: 'number' },
21+
{ value: 'NaN', error: null, type: 'number' },
22+
]
23+
24+
expect(sumNumericResults(results)).toBe(0)
25+
})
26+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { LineResult } from '@/composables/math-notebook'
2+
3+
export function sumNumericResults(results: LineResult[]) {
4+
return results.reduce(
5+
(sum, result) =>
6+
result.numericValue != null ? sum + result.numericValue : sum,
7+
0,
8+
)
9+
}

src/renderer/composables/__tests__/useMathEngine.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/* eslint-disable test/prefer-lowercase-title */
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { preprocessMathExpression } from '../math-notebook/math-engine/preprocess'
34
import { useMathEngine } from '../math-notebook/useMathEngine'
45

56
const TEST_CURRENCY_RATES = {
67
USD: 1,
78
EUR: 0.92,
89
GBP: 0.79,
910
CAD: 1.36,
11+
RUB: 90,
1012
}
1113

1214
const { evaluateDocument, setCurrencyServiceState, updateCurrencyRates }
@@ -461,6 +463,73 @@ describe('number format', () => {
461463
})
462464
})
463465

466+
describe('numericValue for total', () => {
467+
it('sets numericValue for plain number', () => {
468+
const result = evalLine('42')
469+
expect(result.numericValue).toBe(42)
470+
})
471+
472+
it('sets numericValue for arithmetic result', () => {
473+
const result = evalLine('10 + 5')
474+
expect(result.numericValue).toBe(15)
475+
})
476+
477+
it('sets numericValue for number assignment', () => {
478+
const results = evalLines('x = 10')
479+
expect(results[0].numericValue).toBe(10)
480+
})
481+
482+
it('does not set numericValue for date', () => {
483+
const result = evalLine('now')
484+
expect(result.numericValue).toBeUndefined()
485+
})
486+
487+
it('does not set numericValue for date assignment', () => {
488+
const results = evalLines('x = 12.03.2025')
489+
expect(results[0].numericValue).toBeUndefined()
490+
})
491+
492+
it('does not set numericValue for unit result', () => {
493+
const result = evalLine('10 USD')
494+
expect(result.numericValue).toBeUndefined()
495+
})
496+
497+
it('does not set numericValue for unit assignment', () => {
498+
const results = evalLines('price = 10 USD')
499+
expect(results[0].numericValue).toBeUndefined()
500+
})
501+
502+
it('sets intValue for hex format', () => {
503+
const result = evalLine('255 in hex')
504+
expect(result.numericValue).toBe(255)
505+
})
506+
507+
it('sets intValue (rounded) for hex with float', () => {
508+
const result = evalLine('10.6 in hex')
509+
expect(result.numericValue).toBe(11)
510+
})
511+
512+
it('sets intValue for bin format', () => {
513+
const result = evalLine('255 in bin')
514+
expect(result.numericValue).toBe(255)
515+
})
516+
517+
it('sets intValue for oct format', () => {
518+
const result = evalLine('255 in oct')
519+
expect(result.numericValue).toBe(255)
520+
})
521+
522+
it('sets numericValue for sci format', () => {
523+
const result = evalLine('5300 in sci')
524+
expect(result.numericValue).toBe(5300)
525+
})
526+
527+
it('does not set numericValue for Infinity', () => {
528+
const result = evalLine('1/0')
529+
expect(result.numericValue).toBeUndefined()
530+
})
531+
})
532+
464533
describe('area and volume aliases', () => {
465534
it('sq alias', () => {
466535
const result = evalLine('20 sq cm to cm^2')
@@ -578,6 +647,118 @@ describe('sum and total', () => {
578647
})
579648
})
580649

650+
describe('adjacent digit concatenation', () => {
651+
it('concatenates space-separated digits', () => {
652+
expectValue('1 1 2', '112')
653+
})
654+
655+
it('works with operators before grouped digits', () => {
656+
expectValue('1 + 1 + 1 1 2', '114')
657+
})
658+
659+
it('concatenates two digit groups', () => {
660+
expectValue('1 0 + 2 0', '30')
661+
})
662+
663+
it('does not break thousands grouping', () => {
664+
expectValue('4 500', '4,500')
665+
})
666+
667+
it('does not break stacked units', () => {
668+
const result = evalLine('1 meter 20 cm')
669+
expect(result.type).toBe('unit')
670+
expect(result.value).toContain('1.2')
671+
})
672+
})
673+
674+
describe('mixed currency and plain number', () => {
675+
it('adds plain number to currency in addition', () => {
676+
const result = evalLine('$100 + 10')
677+
expect(result.value).toContain('110')
678+
expect(result.type).toBe('unit')
679+
})
680+
681+
it('adds plain number to currency with multiple terms', () => {
682+
const result = evalLine('$100 + $200 + 10')
683+
expect(result.value).toContain('310')
684+
expect(result.type).toBe('unit')
685+
})
686+
687+
it('adds plain number before currency', () => {
688+
const result = evalLine('10 + $100')
689+
expect(result.value).toContain('110')
690+
expect(result.type).toBe('unit')
691+
})
692+
693+
it('subtracts plain number from currency', () => {
694+
const result = evalLine('$100 - 10')
695+
expect(result.value).toContain('90')
696+
expect(result.type).toBe('unit')
697+
})
698+
699+
it('does not modify multiplication with plain number', () => {
700+
const result = evalLine('$100 * 2')
701+
expect(result.value).toContain('200')
702+
expect(result.type).toBe('unit')
703+
})
704+
705+
it('does not modify division with plain number', () => {
706+
const result = evalLine('$100 / 4')
707+
expect(result.value).toContain('25')
708+
expect(result.type).toBe('unit')
709+
})
710+
711+
it('does not modify expression without currency', () => {
712+
expectValue('10 + 20', '30')
713+
})
714+
715+
it('works with word operator plus', () => {
716+
const result = evalLine('$100 plus 10')
717+
expect(result.value).toContain('110')
718+
expect(result.type).toBe('unit')
719+
})
720+
721+
it('works with word operator minus', () => {
722+
const result = evalLine('$100 minus 10')
723+
expect(result.value).toContain('90')
724+
expect(result.type).toBe('unit')
725+
})
726+
727+
it('does not infer currency for percentage operands', () => {
728+
expect(preprocessMathExpression('$100 + 10%')).toBe('100 USD + 10 / 100')
729+
})
730+
731+
it('keeps trailing conversion on the whole inferred expression', () => {
732+
const result = evalLine('10 USD + 1 in RUB')
733+
expect(result.type).toBe('unit')
734+
expect(result.value).toContain('RUB')
735+
expectNumericClose('10 USD + 1 in RUB', 990, 2)
736+
})
737+
})
738+
739+
describe('mixed unit and plain number', () => {
740+
it('adds plain number to day unit', () => {
741+
const result = evalLine('10 day + 34')
742+
expect(result.type).toBe('unit')
743+
expect(result.value).toContain('day')
744+
expectNumericClose('10 day + 34', 44, 2)
745+
})
746+
747+
it('adds plain number before day unit', () => {
748+
const result = evalLine('34 + 10 day')
749+
expect(result.type).toBe('unit')
750+
expect(result.value).toContain('day')
751+
expectNumericClose('34 + 10 day', 44, 2)
752+
})
753+
754+
it('adds plain number after adjacent digit concatenation with unit', () => {
755+
const result = evalLine('1 0 day + 34')
756+
expect(result.type).toBe('unit')
757+
expect(result.value).toContain('day')
758+
expectNumericClose('1 0 day + 34', 44, 2)
759+
})
760+
})
761+
581762
describe('average and avg', () => {
582763
it('average of lines above', () => {
583764
const results = evalLines('10\n20\n30\naverage')

0 commit comments

Comments
 (0)