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
13 changes: 2 additions & 11 deletions src/renderer/components/math-notebook/ResultsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useCopyToClipboard } from '@/composables'
import { formatMathNumber } from '@/composables/math-notebook/math-engine/format'
import { i18n, ipc } from '@/electron'
import { LoaderCircle, Sigma } from 'lucide-vue-next'
import { sumNumericResults } from './sumNumericResults'

interface Props {
results: LineResult[]
Expand All @@ -22,17 +23,7 @@ const MATH_NOTEBOOK_DOCUMENTATION_URL
= 'https://masscode.io/documentation/math-notebook.html'

const total = computed(() => {
return props.results.reduce((sum, r) => {
if (r.type === 'number' || r.type === 'assignment') {
const raw = r.value || ''
if (raw.includes(':'))
return sum
const num = Number.parseFloat(raw.replace(/[^\d.\-e+]/gi, ''))
if (!Number.isNaN(num))
return sum + num
}
return sum
}, 0)
return sumNumericResults(props.results)
})

const formattedTotal = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { LineResult } from '@/composables/math-notebook'
import { describe, expect, it } from 'vitest'
import { sumNumericResults } from '../sumNumericResults'

describe('sumNumericResults', () => {
it('sums numericValue and ignores other result types', () => {
const results: LineResult[] = [
{ value: '10', error: null, type: 'number', numericValue: 10 },
{ value: '12.03.2025, 0:00:00', error: null, type: 'assignment' },
{ value: '100 USD', error: null, type: 'assignment' },
{ value: '0xFF', error: null, type: 'number', numericValue: 255 },
{ value: '5.3e+3', error: null, type: 'number', numericValue: 5300 },
]

expect(sumNumericResults(results)).toBe(5565)
})

it('ignores Infinity and NaN when numericValue is absent', () => {
const results: LineResult[] = [
{ value: 'Infinity', error: null, type: 'number' },
{ value: 'NaN', error: null, type: 'number' },
]

expect(sumNumericResults(results)).toBe(0)
})
})
9 changes: 9 additions & 0 deletions src/renderer/components/math-notebook/sumNumericResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { LineResult } from '@/composables/math-notebook'

export function sumNumericResults(results: LineResult[]) {
return results.reduce(
(sum, result) =>
result.numericValue != null ? sum + result.numericValue : sum,
0,
)
}
181 changes: 181 additions & 0 deletions src/renderer/composables/__tests__/useMathEngine.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* 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 = {
USD: 1,
EUR: 0.92,
GBP: 0.79,
CAD: 1.36,
RUB: 90,
}

const { evaluateDocument, setCurrencyServiceState, updateCurrencyRates }
Expand Down Expand Up @@ -461,6 +463,73 @@ describe('number format', () => {
})
})

describe('numericValue for total', () => {
it('sets numericValue for plain number', () => {
const result = evalLine('42')
expect(result.numericValue).toBe(42)
})

it('sets numericValue for arithmetic result', () => {
const result = evalLine('10 + 5')
expect(result.numericValue).toBe(15)
})

it('sets numericValue for number assignment', () => {
const results = evalLines('x = 10')
expect(results[0].numericValue).toBe(10)
})

it('does not set numericValue for date', () => {
const result = evalLine('now')
expect(result.numericValue).toBeUndefined()
})

it('does not set numericValue for date assignment', () => {
const results = evalLines('x = 12.03.2025')
expect(results[0].numericValue).toBeUndefined()
})

it('does not set numericValue for unit result', () => {
const result = evalLine('10 USD')
expect(result.numericValue).toBeUndefined()
})

it('does not set numericValue for unit assignment', () => {
const results = evalLines('price = 10 USD')
expect(results[0].numericValue).toBeUndefined()
})

it('sets intValue for hex format', () => {
const result = evalLine('255 in hex')
expect(result.numericValue).toBe(255)
})

it('sets intValue (rounded) for hex with float', () => {
const result = evalLine('10.6 in hex')
expect(result.numericValue).toBe(11)
})

it('sets intValue for bin format', () => {
const result = evalLine('255 in bin')
expect(result.numericValue).toBe(255)
})

it('sets intValue for oct format', () => {
const result = evalLine('255 in oct')
expect(result.numericValue).toBe(255)
})

it('sets numericValue for sci format', () => {
const result = evalLine('5300 in sci')
expect(result.numericValue).toBe(5300)
})

it('does not set numericValue for Infinity', () => {
const result = evalLine('1/0')
expect(result.numericValue).toBeUndefined()
})
})

describe('area and volume aliases', () => {
it('sq alias', () => {
const result = evalLine('20 sq cm to cm^2')
Expand Down Expand Up @@ -578,6 +647,118 @@ describe('sum and total', () => {
})
})

describe('adjacent digit concatenation', () => {
it('concatenates space-separated digits', () => {
expectValue('1 1 2', '112')
})

it('works with operators before grouped digits', () => {
expectValue('1 + 1 + 1 1 2', '114')
})

it('concatenates two digit groups', () => {
expectValue('1 0 + 2 0', '30')
})

it('does not break thousands grouping', () => {
expectValue('4 500', '4,500')
})

it('does not break stacked units', () => {
const result = evalLine('1 meter 20 cm')
expect(result.type).toBe('unit')
expect(result.value).toContain('1.2')
})
})

describe('mixed currency and plain number', () => {
it('adds plain number to currency in addition', () => {
const result = evalLine('$100 + 10')
expect(result.value).toContain('110')
expect(result.type).toBe('unit')
})

it('adds plain number to currency with multiple terms', () => {
const result = evalLine('$100 + $200 + 10')
expect(result.value).toContain('310')
expect(result.type).toBe('unit')
})

it('adds plain number before currency', () => {
const result = evalLine('10 + $100')
expect(result.value).toContain('110')
expect(result.type).toBe('unit')
})

it('subtracts plain number from currency', () => {
const result = evalLine('$100 - 10')
expect(result.value).toContain('90')
expect(result.type).toBe('unit')
})

it('does not modify multiplication with plain number', () => {
const result = evalLine('$100 * 2')
expect(result.value).toContain('200')
expect(result.type).toBe('unit')
})

it('does not modify division with plain number', () => {
const result = evalLine('$100 / 4')
expect(result.value).toContain('25')
expect(result.type).toBe('unit')
})

it('does not modify expression without currency', () => {
expectValue('10 + 20', '30')
})

it('works with word operator plus', () => {
const result = evalLine('$100 plus 10')
expect(result.value).toContain('110')
expect(result.type).toBe('unit')
})

it('works with word operator minus', () => {
const result = evalLine('$100 minus 10')
expect(result.value).toContain('90')
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')
expect(result.value).toContain('RUB')
expectNumericClose('10 USD + 1 in RUB', 990, 2)
})
})

describe('mixed unit and plain number', () => {
it('adds plain number to day unit', () => {
const result = evalLine('10 day + 34')
expect(result.type).toBe('unit')
expect(result.value).toContain('day')
expectNumericClose('10 day + 34', 44, 2)
})

it('adds plain number before day unit', () => {
const result = evalLine('34 + 10 day')
expect(result.type).toBe('unit')
expect(result.value).toContain('day')
expectNumericClose('34 + 10 day', 44, 2)
})

it('adds plain number after adjacent digit concatenation with unit', () => {
const result = evalLine('1 0 day + 34')
expect(result.type).toBe('unit')
expect(result.value).toContain('day')
expectNumericClose('1 0 day + 34', 44, 2)
})
})

describe('average and avg', () => {
it('average of lines above', () => {
const results = evalLines('10\n20\n30\naverage')
Expand Down
Loading