@actual-app/core 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- package/vitest.web.config.ts +38 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
3
|
+
import type { Locale } from 'date-fns';
|
|
4
|
+
|
|
5
|
+
export function last<T>(arr: Array<T>) {
|
|
6
|
+
return arr[arr.length - 1];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getChangedValues<T extends { id?: string }>(obj1: T, obj2: T) {
|
|
10
|
+
const diff: Partial<T> = {};
|
|
11
|
+
const keys = Object.keys(obj2);
|
|
12
|
+
let hasChanged = false;
|
|
13
|
+
|
|
14
|
+
// Keep the id field because this is mostly used to diff database
|
|
15
|
+
// objects
|
|
16
|
+
if (obj1.id) {
|
|
17
|
+
diff.id = obj1.id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < keys.length; i++) {
|
|
21
|
+
const key = keys[i];
|
|
22
|
+
|
|
23
|
+
if (obj1[key] !== obj2[key]) {
|
|
24
|
+
diff[key] = obj2[key];
|
|
25
|
+
hasChanged = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hasChanged ? diff : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasFieldsChanged<T extends object>(
|
|
33
|
+
obj1: T,
|
|
34
|
+
obj2: T,
|
|
35
|
+
fields: Array<keyof T>,
|
|
36
|
+
) {
|
|
37
|
+
let changed = false;
|
|
38
|
+
for (let i = 0; i < fields.length; i++) {
|
|
39
|
+
const field = fields[i];
|
|
40
|
+
if (obj1[field] !== obj2[field]) {
|
|
41
|
+
changed = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return changed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type Diff<T extends { id: string }> = {
|
|
49
|
+
added: T[];
|
|
50
|
+
updated: Partial<T>[];
|
|
51
|
+
deleted: Pick<T, 'id'>[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function applyChanges<T extends { id: string }>(
|
|
55
|
+
changes: Diff<T>,
|
|
56
|
+
items: T[],
|
|
57
|
+
) {
|
|
58
|
+
items = [...items];
|
|
59
|
+
|
|
60
|
+
if (changes.added) {
|
|
61
|
+
changes.added.forEach(add => {
|
|
62
|
+
items.push(add);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (changes.updated) {
|
|
67
|
+
changes.updated.forEach(({ id, ...fields }) => {
|
|
68
|
+
const idx = items.findIndex(t => t.id === id);
|
|
69
|
+
items[idx] = {
|
|
70
|
+
...items[idx],
|
|
71
|
+
...fields,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (changes.deleted) {
|
|
77
|
+
changes.deleted.forEach(t => {
|
|
78
|
+
const idx = items.findIndex(t2 => t.id === t2.id);
|
|
79
|
+
if (idx !== -1) {
|
|
80
|
+
items.splice(idx, 1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return items;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function partitionByField<T, K extends keyof T>(data: T[], field: K) {
|
|
89
|
+
const res = new Map();
|
|
90
|
+
for (let i = 0; i < data.length; i++) {
|
|
91
|
+
const item = data[i];
|
|
92
|
+
const key = item[field];
|
|
93
|
+
|
|
94
|
+
const items = res.get(key) || [];
|
|
95
|
+
items.push(item);
|
|
96
|
+
|
|
97
|
+
res.set(key, items);
|
|
98
|
+
}
|
|
99
|
+
return res;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function groupBy<T, K extends keyof T>(data: T[], field: K) {
|
|
103
|
+
const res = new Map<T[K], T[]>();
|
|
104
|
+
for (let i = 0; i < data.length; i++) {
|
|
105
|
+
const item = data[i];
|
|
106
|
+
const key = item[field];
|
|
107
|
+
const existing = res.get(key) || [];
|
|
108
|
+
res.set(key, existing.concat([item]));
|
|
109
|
+
}
|
|
110
|
+
return res;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// This should replace the existing `groupById` function, since a
|
|
114
|
+
// `Map` is better, but we can't swap it out because `Map` has a
|
|
115
|
+
// different API and we need to go through and update everywhere that
|
|
116
|
+
// uses it.
|
|
117
|
+
function _groupById<T extends { id: string }>(data: T[]) {
|
|
118
|
+
const res = new Map<string, T>();
|
|
119
|
+
for (let i = 0; i < data.length; i++) {
|
|
120
|
+
const item = data[i];
|
|
121
|
+
res.set(item.id, item);
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function diffItems<T extends { id: string }>(
|
|
127
|
+
items: T[],
|
|
128
|
+
newItems: T[],
|
|
129
|
+
): Diff<T> {
|
|
130
|
+
const grouped = _groupById(items);
|
|
131
|
+
const newGrouped = _groupById(newItems);
|
|
132
|
+
const added: T[] = [];
|
|
133
|
+
const updated: Partial<T>[] = [];
|
|
134
|
+
|
|
135
|
+
const deleted: Pick<T, 'id'>[] = items
|
|
136
|
+
.filter(item => !newGrouped.has(item.id))
|
|
137
|
+
.map(item => ({ id: item.id }));
|
|
138
|
+
|
|
139
|
+
newItems.forEach(newItem => {
|
|
140
|
+
const item = grouped.get(newItem.id);
|
|
141
|
+
if (!item) {
|
|
142
|
+
added.push(newItem);
|
|
143
|
+
} else {
|
|
144
|
+
const changes = getChangedValues(item, newItem);
|
|
145
|
+
if (changes) {
|
|
146
|
+
updated.push(changes);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return { added, updated, deleted };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function groupById<T extends { id: string }>(
|
|
155
|
+
data: T[] | null | undefined,
|
|
156
|
+
): Record<string, T> {
|
|
157
|
+
if (!data) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
const res: { [key: string]: T } = {};
|
|
161
|
+
for (let i = 0; i < data.length; i++) {
|
|
162
|
+
const item = data[i];
|
|
163
|
+
res[item.id] = item;
|
|
164
|
+
}
|
|
165
|
+
return res;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function setIn(
|
|
169
|
+
map: Map<string, unknown>,
|
|
170
|
+
keys: string[],
|
|
171
|
+
item: unknown,
|
|
172
|
+
): void {
|
|
173
|
+
for (let i = 0; i < keys.length; i++) {
|
|
174
|
+
const key = keys[i];
|
|
175
|
+
|
|
176
|
+
if (i === keys.length - 1) {
|
|
177
|
+
map.set(key, item);
|
|
178
|
+
} else {
|
|
179
|
+
if (!map.has(key)) {
|
|
180
|
+
map.set(key, new Map<string, unknown>());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
map = map.get(key) as Map<string, unknown>;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getIn(map, keys) {
|
|
189
|
+
let item = map;
|
|
190
|
+
for (let i = 0; i < keys.length; i++) {
|
|
191
|
+
item = item.get(keys[i]);
|
|
192
|
+
|
|
193
|
+
if (item == null) {
|
|
194
|
+
return item;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return item;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function fastSetMerge<T>(set1: Set<T>, set2: Set<T>) {
|
|
201
|
+
const finalSet = new Set(set1);
|
|
202
|
+
const iter = set2.values();
|
|
203
|
+
let value = iter.next();
|
|
204
|
+
while (!value.done) {
|
|
205
|
+
finalSet.add(value.value);
|
|
206
|
+
value = iter.next();
|
|
207
|
+
}
|
|
208
|
+
return finalSet;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function titleFirst(str: string | null | undefined) {
|
|
212
|
+
if (!str || str.length <= 1) {
|
|
213
|
+
return str?.toUpperCase() ?? '';
|
|
214
|
+
}
|
|
215
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function reapplyThousandSeparators(amountText: string) {
|
|
219
|
+
if (!amountText || typeof amountText !== 'string') {
|
|
220
|
+
return amountText;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { decimalSeparator, thousandsSeparator, value } = getNumberFormat();
|
|
224
|
+
const [integerPartRaw, decimalPart = ''] = amountText.split(decimalSeparator);
|
|
225
|
+
|
|
226
|
+
// Apostrophe-dot: accept both U+2019 and keyboard U+0027 on input (see getNumberFormat formatter)
|
|
227
|
+
const stripThousands =
|
|
228
|
+
value === 'apostrophe-dot'
|
|
229
|
+
? (s: string) => s.replaceAll(/[\u2019\u0027]/g, '')
|
|
230
|
+
: (s: string) => s.replaceAll(thousandsSeparator, '');
|
|
231
|
+
const numericValue = Number(stripThousands(integerPartRaw));
|
|
232
|
+
if (isNaN(numericValue)) {
|
|
233
|
+
return amountText; // Return original if parsing fails
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const integerPart = numericValue
|
|
237
|
+
.toLocaleString('en-US')
|
|
238
|
+
.replaceAll(',', thousandsSeparator);
|
|
239
|
+
return decimalPart
|
|
240
|
+
? integerPart + decimalSeparator + decimalPart
|
|
241
|
+
: integerPart;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function appendDecimals(
|
|
245
|
+
amountText: string,
|
|
246
|
+
hideDecimals = false,
|
|
247
|
+
): string {
|
|
248
|
+
const { decimalSeparator: separator } = getNumberFormat();
|
|
249
|
+
let result = amountText;
|
|
250
|
+
if (result.slice(-1) === separator) {
|
|
251
|
+
result = result.slice(0, -1);
|
|
252
|
+
}
|
|
253
|
+
if (!hideDecimals) {
|
|
254
|
+
result = result.replaceAll(/[,.]/g, '');
|
|
255
|
+
result = result.replace(/^0+(?!$)/, '');
|
|
256
|
+
result = result.padStart(3, '0');
|
|
257
|
+
result = result.slice(0, -2) + separator + result.slice(-2);
|
|
258
|
+
}
|
|
259
|
+
return amountToCurrency(currencyToAmount(result));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const NUMBER_FORMATS = [
|
|
263
|
+
'comma-dot',
|
|
264
|
+
'dot-comma',
|
|
265
|
+
'space-comma',
|
|
266
|
+
'apostrophe-dot',
|
|
267
|
+
'comma-dot',
|
|
268
|
+
'comma-dot-in',
|
|
269
|
+
] as const;
|
|
270
|
+
|
|
271
|
+
export type NumberFormats = (typeof NUMBER_FORMATS)[number];
|
|
272
|
+
|
|
273
|
+
function isNumberFormat(input: string = ''): input is NumberFormats {
|
|
274
|
+
return (NUMBER_FORMATS as readonly string[]).includes(input);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const numberFormats: Array<{
|
|
278
|
+
value: NumberFormats;
|
|
279
|
+
label: string;
|
|
280
|
+
labelNoFraction: string;
|
|
281
|
+
}> = [
|
|
282
|
+
{ value: 'comma-dot', label: '1,000.33', labelNoFraction: '1,000' },
|
|
283
|
+
{ value: 'dot-comma', label: '1.000,33', labelNoFraction: '1.000' },
|
|
284
|
+
{
|
|
285
|
+
value: 'space-comma',
|
|
286
|
+
label: '1\u202F000,33',
|
|
287
|
+
labelNoFraction: '1\u202F000',
|
|
288
|
+
},
|
|
289
|
+
{ value: 'apostrophe-dot', label: "1'000.33", labelNoFraction: "1'000" },
|
|
290
|
+
{ value: 'comma-dot-in', label: '1,00,000.33', labelNoFraction: '1,00,000' },
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
let numberFormatConfig: {
|
|
294
|
+
format: NumberFormats;
|
|
295
|
+
hideFraction: boolean;
|
|
296
|
+
} = {
|
|
297
|
+
format: 'comma-dot',
|
|
298
|
+
hideFraction: false,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export function parseNumberFormat({
|
|
302
|
+
format,
|
|
303
|
+
hideFraction,
|
|
304
|
+
}: {
|
|
305
|
+
format?: string;
|
|
306
|
+
hideFraction?: string | boolean;
|
|
307
|
+
}) {
|
|
308
|
+
return {
|
|
309
|
+
format: isNumberFormat(format) ? format : 'comma-dot',
|
|
310
|
+
hideFraction: String(hideFraction) === 'true',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function setNumberFormat(config: typeof numberFormatConfig) {
|
|
315
|
+
numberFormatConfig = config;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getNumberFormat({
|
|
319
|
+
format = numberFormatConfig.format,
|
|
320
|
+
hideFraction = numberFormatConfig.hideFraction,
|
|
321
|
+
decimalPlaces,
|
|
322
|
+
}: {
|
|
323
|
+
format?: NumberFormats;
|
|
324
|
+
hideFraction?: boolean;
|
|
325
|
+
decimalPlaces?: number;
|
|
326
|
+
} = numberFormatConfig) {
|
|
327
|
+
let locale, thousandsSeparator, decimalSeparator;
|
|
328
|
+
|
|
329
|
+
const currentFormat = format || numberFormatConfig.format;
|
|
330
|
+
const currentHideFraction =
|
|
331
|
+
typeof hideFraction === 'boolean'
|
|
332
|
+
? hideFraction
|
|
333
|
+
: numberFormatConfig.hideFraction;
|
|
334
|
+
|
|
335
|
+
switch (format) {
|
|
336
|
+
case 'space-comma':
|
|
337
|
+
locale = 'fr-FR';
|
|
338
|
+
thousandsSeparator = '\u202F';
|
|
339
|
+
decimalSeparator = ',';
|
|
340
|
+
break;
|
|
341
|
+
case 'dot-comma':
|
|
342
|
+
locale = 'de-DE';
|
|
343
|
+
thousandsSeparator = '.';
|
|
344
|
+
decimalSeparator = ',';
|
|
345
|
+
break;
|
|
346
|
+
case 'apostrophe-dot':
|
|
347
|
+
locale = 'de-CH';
|
|
348
|
+
thousandsSeparator = '\u2019'; // Intl may return U+0027 (Node <24.13.1/ICU 77)
|
|
349
|
+
decimalSeparator = '.';
|
|
350
|
+
break;
|
|
351
|
+
case 'comma-dot-in':
|
|
352
|
+
locale = 'en-IN';
|
|
353
|
+
thousandsSeparator = ',';
|
|
354
|
+
decimalSeparator = '.';
|
|
355
|
+
break;
|
|
356
|
+
case 'comma-dot':
|
|
357
|
+
default:
|
|
358
|
+
locale = 'en-US';
|
|
359
|
+
thousandsSeparator = ',';
|
|
360
|
+
decimalSeparator = '.';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const fractionDigitsOptions: {
|
|
364
|
+
minimumFractionDigits: number;
|
|
365
|
+
maximumFractionDigits: number;
|
|
366
|
+
} =
|
|
367
|
+
typeof decimalPlaces === 'number'
|
|
368
|
+
? {
|
|
369
|
+
minimumFractionDigits: decimalPlaces,
|
|
370
|
+
maximumFractionDigits: decimalPlaces,
|
|
371
|
+
}
|
|
372
|
+
: {
|
|
373
|
+
minimumFractionDigits: currentHideFraction ? 0 : 2,
|
|
374
|
+
maximumFractionDigits: currentHideFraction ? 0 : 2,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const intlFormatter = new Intl.NumberFormat(locale, fractionDigitsOptions);
|
|
378
|
+
|
|
379
|
+
// Wrapper to handle -0 edge case
|
|
380
|
+
// Normalize apostrophe-dot to U+2019 for consistency across
|
|
381
|
+
// Node/ICU versions (https://github.com/nodejs/node/issues/61861)
|
|
382
|
+
const formatter = {
|
|
383
|
+
format: (value: number) => {
|
|
384
|
+
let formatted = intlFormatter.format(value);
|
|
385
|
+
if (currentFormat === 'apostrophe-dot') {
|
|
386
|
+
formatted = formatted.replace(/'/g, '\u2019');
|
|
387
|
+
}
|
|
388
|
+
return formatted === '-0' ? '0' : formatted;
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
value: currentFormat,
|
|
394
|
+
thousandsSeparator,
|
|
395
|
+
decimalSeparator,
|
|
396
|
+
formatter,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Number utilities
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The exact amount.
|
|
404
|
+
*/
|
|
405
|
+
export type Amount = number;
|
|
406
|
+
/**
|
|
407
|
+
* The exact amount that is formatted based on the configured number format.
|
|
408
|
+
* For example, 123.45 would be '123.45' or '123,45'.
|
|
409
|
+
*/
|
|
410
|
+
export type CurrencyAmount = string;
|
|
411
|
+
/**
|
|
412
|
+
* The amount with the decimal point removed.
|
|
413
|
+
* For example, 123.45 would be 12345.
|
|
414
|
+
*/
|
|
415
|
+
export type IntegerAmount = number;
|
|
416
|
+
|
|
417
|
+
// We dont use `Number.MAX_SAFE_NUMBER` and such here because those
|
|
418
|
+
// numbers are so large that it's not safe to convert them to floats
|
|
419
|
+
// (i.e. N / 100). For example, `9007199254740987 / 100 ===
|
|
420
|
+
// 90071992547409.88`. While the internal arithemetic would be correct
|
|
421
|
+
// because we always do that on numbers, the app would potentially
|
|
422
|
+
// display wrong numbers. Instead of `2**53` we use `2**51` which
|
|
423
|
+
// gives division more room to be correct
|
|
424
|
+
const MAX_SAFE_NUMBER = 2 ** 51 - 1;
|
|
425
|
+
const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER;
|
|
426
|
+
|
|
427
|
+
export function safeNumber(value: number) {
|
|
428
|
+
if (!Number.isInteger(value)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
'safeNumber: number is not an integer: ' + JSON.stringify(value),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
"safeNumber: can't safely perform arithmetic with number: " + value,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function toRelaxedNumber(currencyAmount: CurrencyAmount): Amount {
|
|
442
|
+
return integerToAmount(currencyToInteger(currencyAmount) || 0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function integerToCurrency(
|
|
446
|
+
integerAmount: IntegerAmount,
|
|
447
|
+
formatter = getNumberFormat().formatter,
|
|
448
|
+
decimalPlaces: number = 2,
|
|
449
|
+
) {
|
|
450
|
+
const divisor = Math.pow(10, decimalPlaces);
|
|
451
|
+
const amount = safeNumber(integerAmount) / divisor;
|
|
452
|
+
|
|
453
|
+
return formatter.format(amount);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function integerToCurrencyWithDecimal(integerAmount: IntegerAmount) {
|
|
457
|
+
// If decimal digits exist, keep them. Otherwise format them as usual.
|
|
458
|
+
if (integerAmount % 100 !== 0) {
|
|
459
|
+
return integerToCurrency(
|
|
460
|
+
integerAmount,
|
|
461
|
+
getNumberFormat({
|
|
462
|
+
...numberFormatConfig,
|
|
463
|
+
hideFraction: false,
|
|
464
|
+
}).formatter,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return integerToCurrency(integerAmount);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function amountToCurrency(amount: Amount): CurrencyAmount {
|
|
472
|
+
return getNumberFormat().formatter.format(amount);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function amountToCurrencyNoDecimal(amount: Amount): CurrencyAmount {
|
|
476
|
+
return getNumberFormat({
|
|
477
|
+
...numberFormatConfig,
|
|
478
|
+
hideFraction: true,
|
|
479
|
+
}).formatter.format(amount);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function currencyToAmount(currencyAmount: string): Amount | null {
|
|
483
|
+
currencyAmount = currencyAmount.replace(/\u2212/g, '-');
|
|
484
|
+
|
|
485
|
+
let integer, fraction;
|
|
486
|
+
|
|
487
|
+
// match the last dot or comma in the string
|
|
488
|
+
const match = currencyAmount.match(/[,.](?=[^.,]*$)/);
|
|
489
|
+
|
|
490
|
+
if (
|
|
491
|
+
!match ||
|
|
492
|
+
(match[0] === getNumberFormat().thousandsSeparator &&
|
|
493
|
+
match.index + 4 <= currencyAmount.length)
|
|
494
|
+
) {
|
|
495
|
+
fraction = null;
|
|
496
|
+
integer = currencyAmount.replace(/[^\d-]/g, '');
|
|
497
|
+
} else {
|
|
498
|
+
integer = currencyAmount.slice(0, match.index).replace(/[^\d-]/g, '');
|
|
499
|
+
fraction = currencyAmount.slice(match.index + 1);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const amount = parseFloat(integer + '.' + fraction);
|
|
503
|
+
return isNaN(amount) ? null : amount;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function currencyToInteger(
|
|
507
|
+
currencyAmount: CurrencyAmount,
|
|
508
|
+
): IntegerAmount | null {
|
|
509
|
+
const amount = currencyToAmount(currencyAmount);
|
|
510
|
+
return amount == null ? null : amountToInteger(amount);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function stringToInteger(str: string): number | null {
|
|
514
|
+
const amount = parseInt(
|
|
515
|
+
str.replace(/\u2212/g, '-').replace(/[^-0-9.,]/g, ''),
|
|
516
|
+
);
|
|
517
|
+
if (!isNaN(amount)) {
|
|
518
|
+
return amount;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function amountToInteger(
|
|
524
|
+
amount: Amount,
|
|
525
|
+
decimalPlaces: number = 2,
|
|
526
|
+
): IntegerAmount {
|
|
527
|
+
const multiplier = Math.pow(10, decimalPlaces);
|
|
528
|
+
return Math.round(amount * multiplier);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function integerToAmount(
|
|
532
|
+
integerAmount: IntegerAmount,
|
|
533
|
+
decimalPlaces: number = 2,
|
|
534
|
+
): Amount {
|
|
535
|
+
const divisor = Math.pow(10, decimalPlaces);
|
|
536
|
+
return integerAmount / divisor;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// This is used when the input format could be anything (from
|
|
540
|
+
// financial files and we don't want to parse based on the user's
|
|
541
|
+
// number format, because the user could be importing from many
|
|
542
|
+
// currencies. We extract out the numbers and just ignore separators.
|
|
543
|
+
export function looselyParseAmount(amount: string) {
|
|
544
|
+
function safeNumber(v: number): null | number {
|
|
545
|
+
if (isNaN(v)) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const value = v * 100;
|
|
550
|
+
if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return v;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function extractNumbers(v: string): string {
|
|
558
|
+
return v.replace(/[^0-9-]/g, '');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
amount = amount.trim();
|
|
562
|
+
|
|
563
|
+
if (amount.startsWith('(') && amount.endsWith(')')) {
|
|
564
|
+
// Remove Unicode minus inside parentheses before converting to ASCII minus
|
|
565
|
+
amount = amount.replace(/\u2212/g, '');
|
|
566
|
+
amount = amount.replace('(', '-').replace(')', '');
|
|
567
|
+
} else {
|
|
568
|
+
// Replace Unicode minus with ASCII minus for non-parenthesized amounts
|
|
569
|
+
amount = amount.replace(/\u2212/g, '-');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Look for a decimal marker, then look for either 1-2 or 4-9 decimal places.
|
|
573
|
+
// This avoids matching against 3 places which may not actually be decimal
|
|
574
|
+
const m = amount.match(/[.,]([^.,]{4,9}|[^.,]{1,2})$/);
|
|
575
|
+
if (!m || m.index === undefined) {
|
|
576
|
+
return safeNumber(parseFloat(extractNumbers(amount)));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const left = extractNumbers(amount.slice(0, m.index));
|
|
580
|
+
const right = extractNumbers(amount.slice(m.index + 1));
|
|
581
|
+
|
|
582
|
+
return safeNumber(parseFloat(left + '.' + right));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function sortByKey<T>(arr: T[], key: keyof T): T[] {
|
|
586
|
+
return [...arr].sort((item1, item2) => {
|
|
587
|
+
if (item1[key] < item2[key]) {
|
|
588
|
+
return -1;
|
|
589
|
+
} else if (item1[key] > item2[key]) {
|
|
590
|
+
return 1;
|
|
591
|
+
}
|
|
592
|
+
return 0;
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Date utilities
|
|
597
|
+
|
|
598
|
+
export function tsToRelativeTime(
|
|
599
|
+
ts: string | null,
|
|
600
|
+
locale: Locale,
|
|
601
|
+
options: {
|
|
602
|
+
capitalize: boolean;
|
|
603
|
+
} = { capitalize: false },
|
|
604
|
+
): string {
|
|
605
|
+
if (!ts) return 'Unknown';
|
|
606
|
+
|
|
607
|
+
const parsed = new Date(parseInt(ts, 10));
|
|
608
|
+
|
|
609
|
+
let distance = formatDistanceToNow(parsed, { addSuffix: true, locale });
|
|
610
|
+
|
|
611
|
+
if (options.capitalize) {
|
|
612
|
+
distance = distance.charAt(0).toUpperCase() + distance.slice(1);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return distance;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function applyFindReplace(
|
|
619
|
+
text: string | null | undefined,
|
|
620
|
+
find: string,
|
|
621
|
+
replace: string,
|
|
622
|
+
useRegex: boolean,
|
|
623
|
+
): string {
|
|
624
|
+
if (find === '') return text ?? '';
|
|
625
|
+
if (!text) return '';
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const pattern = useRegex ? new RegExp(find, 'g') : find;
|
|
629
|
+
return text.replaceAll(pattern, replace);
|
|
630
|
+
} catch {
|
|
631
|
+
return text;
|
|
632
|
+
}
|
|
633
|
+
}
|