Skip to content
11 changes: 11 additions & 0 deletions src/main/i18n/locales/en_US/preferences.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@
"label": "Markdown",
"codeRenderer": "Code block Renderer"
},
"math": {
"label": "Math Notebook",
"locale": {
"label": "Region",
"description": "Number and date display format."
},
"decimalPlaces": {
"label": "Decimal Places",
"description": "Maximum decimal places in results (0\u201314)."
}
},
"api": {
"label": "API Port",
"port": {
Expand Down
22 changes: 22 additions & 0 deletions src/main/store/module/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
EditorSettings,
MarkdownSettings,
MathSettings,
NotesEditorSettings,
PreferencesStore,
} from '../types'
Expand All @@ -20,6 +21,11 @@ const isWin = platform() === 'win32'

const storagePath = isWin ? `${homedir()}\\massCode` : `${homedir()}/massCode`

const MATH_DEFAULTS: MathSettings = {
locale: 'en-US',
decimalPlaces: 6,
}

const PREFERENCES_DEFAULTS: PreferencesStore = {
appearance: {
theme: 'auto',
Expand All @@ -41,6 +47,7 @@ const PREFERENCES_DEFAULTS: PreferencesStore = {
scale: 1,
},
},
math: MATH_DEFAULTS,
}

function sanitizeCodeEditorSettings(value: unknown): EditorSettings {
Expand Down Expand Up @@ -143,6 +150,19 @@ function sanitizeMarkdownSettings(value: unknown): MarkdownSettings {
}
}

function sanitizeMathSettings(value: unknown): MathSettings {
const source = asRecord(value)

return {
locale: readString(source, 'locale', MATH_DEFAULTS.locale),
decimalPlaces: readNumber(
source,
'decimalPlaces',
MATH_DEFAULTS.decimalPlaces,
),
}
}

function sanitizePreferences(value: unknown): PreferencesStore {
const source = asRecord(value)
const appearanceSource = asRecord(source.appearance)
Expand All @@ -162,6 +182,7 @@ function sanitizePreferences(value: unknown): PreferencesStore {
= Object.keys(asRecord(editorSource.markdown)).length > 0
? asRecord(editorSource.markdown)
: asRecord(source.markdown)
const mathSource = asRecord(source.math)

return {
appearance: {
Expand Down Expand Up @@ -210,6 +231,7 @@ function sanitizePreferences(value: unknown): PreferencesStore {
notes: sanitizeNotesEditorSettings(notesEditorSource),
markdown: sanitizeMarkdownSettings(markdownSource),
},
math: sanitizeMathSettings(mathSource),
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/store/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export interface NotesEditorSettings {
indentSize: number
}

export interface MathSettings {
locale: string
decimalPlaces: number
}

export interface PreferencesStore {
appearance: {
theme: string
Expand All @@ -96,6 +101,7 @@ export interface PreferencesStore {
notes: NotesEditorSettings
markdown: MarkdownSettings
}
math: MathSettings
}

export interface MathSheet {
Expand Down
12 changes: 8 additions & 4 deletions src/renderer/components/math-notebook/ResultsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
import type { LineResult } from '@/composables/math-notebook'
import { Button } from '@/components/ui/shadcn/button'
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'

interface Props {
results: LineResult[]
scrollTop: number
activeLine: number
locale: string
decimalPlaces: number
}

const props = defineProps<Props>()
Expand All @@ -21,7 +24,10 @@ const MATH_NOTEBOOK_DOCUMENTATION_URL
const total = computed(() => {
return props.results.reduce((sum, r) => {
if (r.type === 'number' || r.type === 'assignment') {
const num = Number.parseFloat((r.value || '').replace(/,/g, ''))
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
}
Expand All @@ -30,9 +36,7 @@ const total = computed(() => {
})

const formattedTotal = computed(() => {
return Number.isInteger(total.value)
? total.value.toLocaleString('en-US')
: total.value.toLocaleString('en-US', { maximumFractionDigits: 6 })
return formatMathNumber(total.value, props.locale, props.decimalPlaces)
})

const resultsStyle = computed(() => {
Expand Down
26 changes: 23 additions & 3 deletions src/renderer/components/math-notebook/Workspace.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { LineResult } from '@/composables/math-notebook'
import type { MathSettings } from '~/main/store/types'
import { useMathEngine, useMathNotebook } from '@/composables'
import { i18n, ipc } from '@/electron'
import { i18n, ipc, store } from '@/electron'
import { Calculator } from 'lucide-vue-next'

interface CurrencyRatesPayload {
Expand All @@ -10,8 +11,25 @@ interface CurrencyRatesPayload {
}

const { activeSheet, updateSheet } = useMathNotebook()
const { evaluateDocument, setCurrencyServiceState, updateCurrencyRates }
= useMathEngine()
const {
evaluateDocument,
setCurrencyServiceState,
setFormatSettings,
updateCurrencyRates,
} = useMathEngine()

const mathSettings = reactive(store.preferences.get('math') as MathSettings)

setFormatSettings(mathSettings.locale, mathSettings.decimalPlaces)

watch(
mathSettings,
() => {
store.preferences.set('math', JSON.parse(JSON.stringify(mathSettings)))
setFormatSettings(mathSettings.locale, mathSettings.decimalPlaces)
},
{ deep: true },
)

const scrollTop = ref(0)
const activeLine = ref(0)
Expand Down Expand Up @@ -98,6 +116,8 @@ function handleActiveLine(line: number) {
:results="results"
:scroll-top="scrollTop"
:active-line="activeLine"
:locale="mathSettings.locale"
:decimal-places="mathSettings.decimalPlaces"
/>
</div>

Expand Down
109 changes: 109 additions & 0 deletions src/renderer/components/preferences/Math.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script setup lang="ts">
import type { MathSettings } from '~/main/store/types'
import * as Select from '@/components/ui/shadcn/select'
import {
formatMathDate,
formatMathNumber,
} from '@/composables/math-notebook/math-engine/format'
import { i18n, store } from '@/electron'

const settings = reactive(store.preferences.get('math') as MathSettings)

watch(
settings,
() => {
store.preferences.set('math', JSON.parse(JSON.stringify(settings)))
},
{ deep: true },
)

const localeOptions = [
{ label: 'English (US)', value: 'en-US' },
{ label: 'English (UK)', value: 'en-GB' },
{ label: 'Deutsch', value: 'de-DE' },
{ label: 'Fran\u00E7ais', value: 'fr-FR' },
{ label: '\u0420\u0443\u0441\u0441\u043A\u0438\u0439', value: 'ru-RU' },
{ label: 'Espa\u00F1ol', value: 'es-ES' },
{ label: 'Portugu\u00EAs (BR)', value: 'pt-BR' },
{ label: '\u65E5\u672C\u8A9E', value: 'ja-JP' },
{ label: '\u4E2D\u6587', value: 'zh-CN' },
{ label: '\uD55C\uAD6D\uC5B4', value: 'ko-KR' },
{ label: 'Italiano', value: 'it-IT' },
{ label: 'Polski', value: 'pl-PL' },
{
label: '\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430',
value: 'uk-UA',
},
{ label: 'T\u00FCrk\u00E7e', value: 'tr-TR' },
{ label: 'Nederlands', value: 'nl-NL' },
]

const decimalPlacesOptions = Array.from({ length: 15 }, (_, i) => ({
label: String(i),
value: String(i),
}))

const localePreview = computed(() => {
const num = formatMathNumber(
1234.5678,
settings.locale,
settings.decimalPlaces,
)
const date = formatMathDate(new Date(), settings.locale)
return `${num} \u00B7 ${date}`
})

const decimalPlacesPreview = computed(() => {
return `1/3 = ${formatMathNumber(1 / 3, settings.locale, settings.decimalPlaces)}`
})
</script>

<template>
<div class="space-y-4">
<UiMenuFormSection :label="i18n.t('preferences:math.label')">
<UiMenuFormItem :label="i18n.t('preferences:math.locale.label')">
<Select.Select v-model="settings.locale">
<Select.SelectTrigger class="w-64">
<Select.SelectValue />
</Select.SelectTrigger>
<Select.SelectContent>
<Select.SelectItem
v-for="option in localeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.SelectItem>
</Select.SelectContent>
</Select.Select>
<template #description>
{{ i18n.t("preferences:math.locale.description") }}
{{ localePreview }}
</template>
</UiMenuFormItem>
<UiMenuFormItem :label="i18n.t('preferences:math.decimalPlaces.label')">
<Select.Select
:model-value="String(settings.decimalPlaces)"
@update:model-value="settings.decimalPlaces = Number($event)"
>
<Select.SelectTrigger class="w-64">
<Select.SelectValue />
</Select.SelectTrigger>
<Select.SelectContent>
<Select.SelectItem
v-for="option in decimalPlacesOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</Select.SelectItem>
</Select.SelectContent>
</Select.Select>
<template #description>
{{ i18n.t("preferences:math.decimalPlaces.description") }}
{{ decimalPlacesPreview }}
</template>
</UiMenuFormItem>
</UiMenuFormSection>
</div>
</template>
18 changes: 11 additions & 7 deletions src/renderer/composables/__tests__/useMathEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ describe('prev', () => {
const results = evalLines('fromunix(1446587186)\nprev + 1 day')
expect(results[1].type).toBe('date')
expect(results[1].value).toBe(
new Date((1446587186 + 86400) * 1000).toLocaleString(),
new Date((1446587186 + 86400) * 1000).toLocaleString('en-US'),
)
})
})
Expand Down Expand Up @@ -658,7 +658,7 @@ describe('fromunix', () => {
const result = evalLine('fromunix(1446587186) + 2 day')
expect(result.type).toBe('date')
expect(result.value).toBe(
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString(),
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString('en-US'),
)
})

Expand All @@ -667,16 +667,20 @@ describe('fromunix', () => {
expect(results[0].type).toBe('assignment')
expect(results[1].type).toBe('date')
expect(results[1].value).toBe(
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString(),
new Date((1446587186 + 2 * 86400) * 1000).toLocaleString('en-US'),
)
})

it('local dotted date assignment + 2 day', () => {
const results = evalLines('x = 12.03.2025\nx + 2 day')
expect(results[0].type).toBe('assignment')
expect(results[0].value).toBe(new Date(2025, 2, 12).toLocaleString())
expect(results[0].value).toBe(
new Date(2025, 2, 12).toLocaleString('en-US'),
)
expect(results[1].type).toBe('date')
expect(results[1].value).toBe(new Date(2025, 2, 14).toLocaleString())
expect(results[1].value).toBe(
new Date(2025, 2, 14).toLocaleString('en-US'),
)
})
})

Expand Down Expand Up @@ -706,7 +710,7 @@ describe('time zones', () => {
const result = evalLine('time() + 1 day')
expect(result.type).toBe('date')
expect(result.value).toBe(
new Date('2026-03-07T12:00:00Z').toLocaleString(),
new Date('2026-03-07T12:00:00Z').toLocaleString('en-US'),
)
})

Expand All @@ -720,7 +724,7 @@ describe('time zones', () => {
const result = evalLine('now + 1 day')
expect(result.type).toBe('date')
expect(result.value).toBe(
new Date('2026-03-07T12:00:00Z').toLocaleString(),
new Date('2026-03-07T12:00:00Z').toLocaleString('en-US'),
)
})

Expand Down
Loading