From 7a821abbb65261f60e732f3c8bd05bba996830ce Mon Sep 17 00:00:00 2001 From: MaysWind Date: Thu, 16 Apr 2026 01:24:07 +0800 Subject: [PATCH] add skewness and kurtosis to value metric in insights explorer --- src/components/desktop/AmountInput.vue | 5 +- src/components/mobile/NumberPadSheet.vue | 3 +- src/consts/numeral.ts | 1 + src/core/explorer.ts | 6 +- src/lib/evaluator.ts | 3 +- src/lib/math.ts | 79 ++++++++++++++++++++ src/lib/numeral.ts | 17 +++-- src/locales/de.json | 2 + src/locales/en.json | 2 + src/locales/es.json | 2 + src/locales/fr.json | 2 + src/locales/helpers.ts | 4 +- src/locales/it.json | 2 + src/locales/ja.json | 2 + src/locales/kn.json | 2 + src/locales/ko.json | 2 + src/locales/nl.json | 2 + src/locales/pt_BR.json | 2 + src/locales/ru.json | 2 + src/locales/sl.json | 2 + src/locales/ta.json | 2 + src/locales/th.json | 2 + src/locales/tr.json | 2 + src/locales/uk.json | 2 + src/locales/vi.json | 2 + src/locales/zh_Hans.json | 2 + src/locales/zh_Hant.json | 2 + src/stores/explorer.ts | 73 +++++++++--------- src/views/desktop/exchangerates/ListPage.vue | 3 +- src/views/mobile/exchangerates/ListPage.vue | 3 +- 30 files changed, 181 insertions(+), 54 deletions(-) diff --git a/src/components/desktop/AmountInput.vue b/src/components/desktop/AmountInput.vue index 0b1e2fbd..82f95ecc 100644 --- a/src/components/desktop/AmountInput.vue +++ b/src/components/desktop/AmountInput.vue @@ -81,8 +81,9 @@ import { import { NumeralSystem, DecimalSeparator } from '@/core/numeral.ts'; import type { CurrencyPrependAndAppendText } from '@/core/currency.ts'; -import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts'; +import { DEFAULT_DECIMAL_NUMBER_COUNT, AMOUNT_FACTOR } from '@/consts/numeral.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; + import { isNumber, replaceAll } from '@/lib/common.ts'; import { evaluateExpressionToAmount } from '@/lib/evaluator.ts'; import type { ComponentDensity, InputVariant } from '@/lib/ui/desktop.ts'; @@ -297,7 +298,7 @@ function getFormattedValue(value: number): string { function getDisplayCurrencyPrependAndAppendText(): CurrencyPrependAndAppendText | null { const numericCurrentValue = parseAmountFromLocalizedNumerals(currentValue.value); - const isPlural = numericCurrentValue !== 100 && numericCurrentValue !== -100; + const isPlural = numericCurrentValue !== AMOUNT_FACTOR && numericCurrentValue !== -AMOUNT_FACTOR; return getAmountPrependAndAppendText(props.currency, isPlural); } diff --git a/src/components/mobile/NumberPadSheet.vue b/src/components/mobile/NumberPadSheet.vue index e0c0b248..79a0e2a4 100644 --- a/src/components/mobile/NumberPadSheet.vue +++ b/src/components/mobile/NumberPadSheet.vue @@ -83,6 +83,7 @@ import { useI18n } from '@/locales/helpers.ts'; import { useI18nUIComponents, isiOS } from '@/lib/ui/mobile.ts'; import { NumeralSystem } from '@/core/numeral.ts'; +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { isNumber } from '@/lib/common.ts'; import logger from '@/lib/logger.ts'; @@ -385,7 +386,7 @@ function confirm(): boolean { finalValue = previous - current; break; case '×': - finalValue = Math.trunc(previous * current / 100); + finalValue = Math.trunc(previous * current / AMOUNT_FACTOR); break; default: finalValue = previous; diff --git a/src/consts/numeral.ts b/src/consts/numeral.ts index 591ef35a..c4de98e2 100644 --- a/src/consts/numeral.ts +++ b/src/consts/numeral.ts @@ -2,6 +2,7 @@ import type { HiddenAmount } from '@/core/numeral.ts'; export const DEFAULT_DECIMAL_NUMBER_COUNT: number = 2; export const MAX_SUPPORTED_DECIMAL_NUMBER_COUNT: number = 2; +export const AMOUNT_FACTOR: number = 10 ** MAX_SUPPORTED_DECIMAL_NUMBER_COUNT; export const DISPLAY_HIDDEN_AMOUNT: HiddenAmount = '***'; export const INCOMPLETE_AMOUNT_SUFFIX: string = '+'; diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 2a626a42..f3652b78 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -324,7 +324,9 @@ export enum TransactionExplorerValueMetricType { SourceAmountInterquartileRange = 'sourceAmountInterquartileRange', SourceAmountVariance = 'sourceAmountVariance', SourceAmountStandardDeviation = 'sourceAmountStandardDeviation', - SourceAmountCoefficientOfVariation = 'sourceAmountCoefficientOfVariation' + SourceAmountCoefficientOfVariation = 'sourceAmountCoefficientOfVariation', + SourceAmountSkewness = 'sourceAmountSkewness', + SourceAmountKurtosis = 'sourceAmountKurtosis' } export class TransactionExplorerValueMetric implements NameValue { @@ -356,6 +358,8 @@ export class TransactionExplorerValueMetric implements NameValue { public static readonly SourceAmountVariance = new TransactionExplorerValueMetric('Variance', TransactionExplorerValueMetricType.SourceAmountVariance, false, false, false); public static readonly SourceAmountStandardDeviation = new TransactionExplorerValueMetric('Standard Deviation', TransactionExplorerValueMetricType.SourceAmountStandardDeviation, false, false, false); public static readonly SourceAmountCoefficientOfVariation = new TransactionExplorerValueMetric('Coefficient of Variation', TransactionExplorerValueMetricType.SourceAmountCoefficientOfVariation, false, false, false); + public static readonly SourceAmountSkewness = new TransactionExplorerValueMetric('Skewness', TransactionExplorerValueMetricType.SourceAmountSkewness, false, false, false); + public static readonly SourceAmountKurtosis = new TransactionExplorerValueMetric('Kurtosis', TransactionExplorerValueMetricType.SourceAmountKurtosis, false, false, false); public static readonly Default = TransactionExplorerValueMetric.SourceAmountSum; diff --git a/src/lib/evaluator.ts b/src/lib/evaluator.ts index f8fbf362..051fc708 100644 --- a/src/lib/evaluator.ts +++ b/src/lib/evaluator.ts @@ -1,3 +1,4 @@ +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '../consts/transaction.ts'; import { replaceAll } from './common.ts'; @@ -10,7 +11,7 @@ type OperatorAndParenthesis = Operator | '(' | ')'; const maxAllowedDecimalCount = 6; const normalizeFactor: number = 1000000; const normalizedDecimalsMaxZeroString: string = '000000'; -const normalizedNumberToAmountFactor: number = 10000; // 1000000 / 100 +const normalizedNumberToAmountFactor: number = normalizeFactor / AMOUNT_FACTOR; const operatorPriority: Record = { '+': 1, diff --git a/src/lib/math.ts b/src/lib/math.ts index cef512c2..10689d76 100644 --- a/src/lib/math.ts +++ b/src/lib/math.ts @@ -1,3 +1,5 @@ +import { reversed } from '@/core/base.ts'; + export function mean(values: T[], valueFn: (item: T) => number): number { if (values.length < 1) { return 0; @@ -59,3 +61,80 @@ export function sumMaxN(sortedValues: T[], n: number, valueFn: (item: T) => n return sum; } + +export function cumulativePercentage(sortedValues: T[], percentageThreshold: number, totalValue: number, valueFn: (item: T) => number): number { + if (sortedValues.length < 1 || percentageThreshold < 0 || percentageThreshold > 1) { + return 0; + } + + const thresholdValue: number = percentageThreshold * totalValue; + let cumulativeValue: number = 0; + let cumulativeCount: number = 0; + + for (const item of reversed(sortedValues)) { + cumulativeValue += valueFn(item); + cumulativeCount++; + + if (cumulativeValue >= thresholdValue) { + return 100.0 * cumulativeCount / sortedValues.length; + } + } + + return 0; +} + +export function varianceAndStandardDeviation(values: T[], meanValue: number, valueFn: (item: T) => number): { variance: number; standardDeviation: number } { + if (values.length < 1) { + return { variance: 0, standardDeviation: 0 }; + } + + let sumOfSquaredDifferences: number = 0; + + for (const item of values) { + const difference: number = valueFn(item) - meanValue; + sumOfSquaredDifferences += difference * difference; + } + + const variance: number = sumOfSquaredDifferences / values.length; + const standardDeviation: number = Math.sqrt(variance); + + return { variance, standardDeviation }; +} + +export function coefficientOfVariation(standardDeviation: number, meanValue: number): number | undefined { + if (meanValue === 0) { + return undefined; + } + + return standardDeviation / meanValue; +} + +export function skewness(values: T[], meanValue: number, standardDeviation: number, valueFn: (item: T) => number): number { + if (values.length < 1 || standardDeviation === 0) { + return 0; + } + + let sumOfCubedDifferences: number = 0; + + for (const item of values) { + const difference: number = valueFn(item) - meanValue; + sumOfCubedDifferences += Math.pow(difference, 3); + } + + return sumOfCubedDifferences / (values.length * Math.pow(standardDeviation, 3)); +} + +export function kurtosis(values: T[], meanValue: number, variance: number, valueFn: (item: T) => number): number { + if (values.length < 1 || variance === 0) { + return 0; + } + + let sumOfQuarticDifferences: number = 0; + + for (const item of values) { + const difference: number = valueFn(item) - meanValue; + sumOfQuarticDifferences += Math.pow(difference, 4); + } + + return sumOfQuarticDifferences / (values.length * Math.pow(variance, 2)); +} diff --git a/src/lib/numeral.ts b/src/lib/numeral.ts index 9282d57f..9dad7ecb 100644 --- a/src/lib/numeral.ts +++ b/src/lib/numeral.ts @@ -6,6 +6,8 @@ import { DigitGroupingSymbol } from '@/core/numeral.ts'; +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; + import { DEFAULT_DECIMAL_NUMBER_COUNT, MAX_SUPPORTED_DECIMAL_NUMBER_COUNT, DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts'; import { isDefined, isString, isNumber, replaceAll, removeAll } from './common.ts'; @@ -115,7 +117,7 @@ export function parseAmount(str: string, options: NumberFormatOptions): number { let decimalSeparatorPos = str.indexOf(decimalSeparator); if (decimalSeparatorPos < 0) { - return sign * numeralSystem.parseInt(str) * 100; + return sign * numeralSystem.parseInt(str) * AMOUNT_FACTOR; } else if (decimalSeparatorPos === 0) { str = numeralSystem.digitZero + str; decimalSeparatorPos++; @@ -125,13 +127,13 @@ export function parseAmount(str: string, options: NumberFormatOptions): number { const decimals = str.substring(decimalSeparatorPos + 1, str.length); if (decimals.length < 1) { - return sign * numeralSystem.parseInt(integer) * 100; + return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR; } else if (decimals.length === 1) { - return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals) * 10; + return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals) * AMOUNT_FACTOR / 10; } else if (decimals.length === 2) { - return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals); + return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals); } else { - return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals.substring(0, 2)); + return sign * numeralSystem.parseInt(integer) * AMOUNT_FACTOR + sign * numeralSystem.parseInt(decimals.substring(0, 2)); } } @@ -252,9 +254,10 @@ export function formatPercent(value: number, precision: number, lowPrecisionValu export function getAmountWithDecimalNumberCount(amount: number, decimalNumberCount: number): number { if (decimalNumberCount === 0) { - return Math.trunc(amount / 100) * 100; + return Math.trunc(amount / AMOUNT_FACTOR) * AMOUNT_FACTOR; } else if (decimalNumberCount === 1) { - return Math.trunc(amount / 10) * 10; + const factor = AMOUNT_FACTOR / 10; + return Math.trunc(amount / factor) * factor; } return amount; diff --git a/src/locales/de.json b/src/locales/de.json index ddc29d21..bbe7e96e 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1824,6 +1824,8 @@ "Variance": "Varianz", "Standard Deviation": "Standardabweichung", "Coefficient of Variation": "Variationskoeffizient", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Kontoliste", "This Week": "Diese Woche", "This Month": "Dieser Monat", diff --git a/src/locales/en.json b/src/locales/en.json index 93970672..54cb0520 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Account List", "This Week": "This Week", "This Month": "This Month", diff --git a/src/locales/es.json b/src/locales/es.json index 7426e849..3d1c0560 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Lista de Cuentas", "This Week": "Esta Semana", "This Month": "Este Mes", diff --git a/src/locales/fr.json b/src/locales/fr.json index 747e2046..25e22870 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Liste des comptes", "This Week": "Cette semaine", "This Month": "Ce mois", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index 766391eb..3010cc55 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -170,7 +170,7 @@ import { import type { LocaleDefaultSettings } from '@/core/setting.ts'; import type { ErrorResponse } from '@/core/api.ts'; -import { DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts'; +import { AMOUNT_FACTOR, DISPLAY_HIDDEN_AMOUNT, INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts'; import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { DEFAULT_EXPENSE_CATEGORIES, DEFAULT_INCOME_CATEGORIES, DEFAULT_TRANSFER_CATEGORIES } from '@/consts/category.ts'; @@ -2189,7 +2189,7 @@ export function useI18n() { const currencyName = getCurrencyName(finalCurrencyCode); if (isNumber(value)) { - const isPlural: boolean = value !== 100 && value !== -100; + const isPlural: boolean = value !== AMOUNT_FACTOR && value !== -AMOUNT_FACTOR; const textualValue = formatAmount(value, numberFormatOptions); if (!finalCurrencyCode) { diff --git a/src/locales/it.json b/src/locales/it.json index 14508ff2..1aedc89e 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Elenco account", "This Week": "Questa settimana", "This Month": "Questo mese", diff --git a/src/locales/ja.json b/src/locales/ja.json index 80149bad..8eb15f0d 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "口座リスト", "This Week": "今週", "This Month": "今月", diff --git a/src/locales/kn.json b/src/locales/kn.json index 4d11d53d..b3dec4ba 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ", "This Week": "ಈ ವಾರ", "This Month": "ಈ ತಿಂಗಳು", diff --git a/src/locales/ko.json b/src/locales/ko.json index 5f897101..7de59665 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "계좌 목록", "This Week": "이번 주", "This Month": "이번 달", diff --git a/src/locales/nl.json b/src/locales/nl.json index b6be2054..c86e2c0f 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Rekeningenlijst", "This Week": "Deze week", "This Month": "Deze maand", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index ea62525f..10c1cc3e 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1824,6 +1824,8 @@ "Variance": "Variância", "Standard Deviation": "Desvio Padrão", "Coefficient of Variation": "Coeficiente de Variação", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Lista de Contas", "This Week": "Esta Semana", "This Month": "Este Mês", diff --git a/src/locales/ru.json b/src/locales/ru.json index d9f28be8..3a485c69 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Список счетов", "This Week": "На этой неделе", "This Month": "В этом месяце", diff --git a/src/locales/sl.json b/src/locales/sl.json index 6c8e5212..44a50f11 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Seznam računov", "This Week": "Ta teden", "This Month": "Ta mesec", diff --git a/src/locales/ta.json b/src/locales/ta.json index b1030ba9..c5e300b1 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "கணக்குகளின் பட்டியல்", "This Week": "இந்த வாரம்", "This Month": "இந்த மாதம்", diff --git a/src/locales/th.json b/src/locales/th.json index 5af33daa..ba8cfcd0 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "รายการบัญชี", "This Week": "สัปดาห์นี้", "This Month": "เดือนนี้", diff --git a/src/locales/tr.json b/src/locales/tr.json index 0554632e..0c14b32a 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Hesap Listesi", "This Week": "Bu Hafta", "This Month": "Bu Ay", diff --git a/src/locales/uk.json b/src/locales/uk.json index 2ec381e2..5a48e6be 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Список рахунків", "This Week": "Цього тижня", "This Month": "Цього місяця", diff --git a/src/locales/vi.json b/src/locales/vi.json index f131cd47..96651347 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1824,6 +1824,8 @@ "Variance": "Variance", "Standard Deviation": "Standard Deviation", "Coefficient of Variation": "Coefficient of Variation", + "Skewness": "Skewness", + "Kurtosis": "Kurtosis", "Account List": "Danh sách tài khoản", "This Week": "Tuần này", "This Month": "Tháng này", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 69785e59..b3a980bd 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1824,6 +1824,8 @@ "Variance": "方差", "Standard Deviation": "标准差", "Coefficient of Variation": "变异系数", + "Skewness": "偏度", + "Kurtosis": "峰度", "Account List": "账户列表", "This Week": "本周", "This Month": "本月", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 3d1a57bc..aa2adbcf 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1824,6 +1824,8 @@ "Variance": "變異數", "Standard Deviation": "標準差", "Coefficient of Variation": "變異係數", + "Skewness": "偏度", + "Kurtosis": "峰度", "Account List": "帳戶清單", "This Week": "本週", "This Month": "本月", diff --git a/src/stores/explorer.ts b/src/stores/explorer.ts index 26f9fa1e..4fc3668d 100644 --- a/src/stores/explorer.ts +++ b/src/stores/explorer.ts @@ -8,7 +8,7 @@ import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useTransactionTagsStore } from './transactionTag.ts'; import { useExchangeRatesStore } from './exchangeRates.ts'; -import { type BeforeResolveFunction, itemAndIndex, reversed, keys, values } from '@/core/base.ts'; +import { type BeforeResolveFunction, itemAndIndex, keys, values } from '@/core/base.ts'; import { NumeralSystem, AmountFilterType } from '@/core/numeral.ts'; import { type DateTime, DateRangeScene, DateRange } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; @@ -20,6 +20,7 @@ import { TransactionExplorerValueMetric, DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE } from '@/core/explorer.ts'; +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { type Account } from '@/models/account.ts'; @@ -46,7 +47,12 @@ import { import { median, percentile, - sumMaxN + sumMaxN, + cumulativePercentage, + varianceAndStandardDeviation, + coefficientOfVariation, + skewness, + kurtosis } from '@/lib/math.ts'; import { getUtcOffsetByUtcOffsetMinutes, @@ -726,26 +732,15 @@ export const useExplorersStore = defineStore('explorers', () => { } if (sourceAmounts.length > 0) { - const eightyPercentAmountThreshold: number = 0.8 * statisticData.totalAmount; - let cumulativeAmount: number = 0; - let cumulativeCount: number = 0; - for (const amount of reversed(sourceAmounts)) { - cumulativeAmount += amount; - cumulativeCount++; - - if (cumulativeAmount >= eightyPercentAmountThreshold) { - statisticData.transactionsFor80PercentAmount = 100.0 * cumulativeCount / sourceAmounts.length; - break; - } - } + statisticData.transactionsFor80PercentAmount = cumulativePercentage(sourceAmounts, 0.8, statisticData.totalAmount, item => item); } if (sourceAmounts.length > 0) { - const averageAmountForVarianceCalculation: number = statisticData.totalAmount / sourceAmounts.length / 100.0; - const sumOfSquaredDifferences: number = sourceAmounts.reduce((sum, amount) => sum + Math.pow(amount / 100.0 - averageAmountForVarianceCalculation, 2), 0); - statisticData.variance = sumOfSquaredDifferences / sourceAmounts.length; - statisticData.standardDeviation = Math.sqrt(statisticData.variance); - statisticData.coefficientOfVariation = averageAmountForVarianceCalculation !== 0 ? statisticData.standardDeviation / averageAmountForVarianceCalculation : undefined; + const averageAmountForVarianceCalculation: number = statisticData.totalAmount / sourceAmounts.length / AMOUNT_FACTOR; + const { variance, standardDeviation } = varianceAndStandardDeviation(sourceAmounts, averageAmountForVarianceCalculation, item => item / AMOUNT_FACTOR); + statisticData.variance = variance; + statisticData.standardDeviation = standardDeviation; + statisticData.coefficientOfVariation = coefficientOfVariation(standardDeviation, averageAmountForVarianceCalculation); } return statisticData; @@ -892,7 +887,12 @@ export const useExplorersStore = defineStore('explorers', () => { } else { value = 0; } - } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountQ1Amount || valueMetric === TransactionExplorerValueMetric.SourceAmountQ3Amount || valueMetric === TransactionExplorerValueMetric.SourceAmount10thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount90thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount95thPercentile || valueMetric === TransactionExplorerValueMetric.SourceAmount99thPercentile) { + } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountQ1Amount + || valueMetric === TransactionExplorerValueMetric.SourceAmountQ3Amount + || valueMetric === TransactionExplorerValueMetric.SourceAmount10thPercentile + || valueMetric === TransactionExplorerValueMetric.SourceAmount90thPercentile + || valueMetric === TransactionExplorerValueMetric.SourceAmount95thPercentile + || valueMetric === TransactionExplorerValueMetric.SourceAmount99thPercentile) { if (allSourceAmountsInDefaultCurrency.length > 0) { allSourceAmountsInDefaultCurrency.sort((a, b) => a - b); @@ -932,18 +932,7 @@ export const useExplorersStore = defineStore('explorers', () => { } else if (valueMetric === TransactionExplorerValueMetric.TransactionsForEightyPercentOfSourceAmount) { if (allSourceAmountsInDefaultCurrency.length > 0) { allSourceAmountsInDefaultCurrency.sort((a, b) => a - b); - const eightyPercentAmountThreshold: number = 0.8 * totalSourceAmountSumInDefaultCurrency; - let cumulativeAmount: number = 0; - let cumulativeCount: number = 0; - for (const amount of reversed(allSourceAmountsInDefaultCurrency)) { - cumulativeAmount += amount; - cumulativeCount++; - - if (cumulativeAmount >= eightyPercentAmountThreshold) { - value = 100.0 * cumulativeCount / allSourceAmountsInDefaultCurrency.length; - break; - } - } + value = cumulativePercentage(allSourceAmountsInDefaultCurrency, 0.8, totalSourceAmountSumInDefaultCurrency, item => item); } else { value = 0; } @@ -962,17 +951,25 @@ export const useExplorersStore = defineStore('explorers', () => { } else { value = 0; } - } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance || valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation || valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation) { + } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance + || valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation + || valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation + || valueMetric === TransactionExplorerValueMetric.SourceAmountSkewness + || valueMetric === TransactionExplorerValueMetric.SourceAmountKurtosis) { if (allSourceAmountsInDefaultCurrency.length > 0) { - const averageSourceAmountInDefaultCurrency = totalSourceAmountSumInDefaultCurrency / allSourceAmountsInDefaultCurrency.length / 100.0; - const sumOfSquaredDifferences = allSourceAmountsInDefaultCurrency.reduce((sum, amount) => sum + Math.pow(amount / 100.0 - averageSourceAmountInDefaultCurrency, 2), 0); + const averageSourceAmountInDefaultCurrency = totalSourceAmountSumInDefaultCurrency / allSourceAmountsInDefaultCurrency.length / AMOUNT_FACTOR; + const { variance, standardDeviation } = varianceAndStandardDeviation(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, item => item / AMOUNT_FACTOR); if (valueMetric === TransactionExplorerValueMetric.SourceAmountVariance) { - value = sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length + value = variance; } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountStandardDeviation) { - value = Math.sqrt(sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length); + value = standardDeviation; } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountCoefficientOfVariation) { - value = averageSourceAmountInDefaultCurrency !== 0 ? Math.sqrt(sumOfSquaredDifferences / allSourceAmountsInDefaultCurrency.length) / averageSourceAmountInDefaultCurrency : 0; + value = coefficientOfVariation(standardDeviation, averageSourceAmountInDefaultCurrency) ?? 0; + } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountSkewness) { + value = skewness(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, standardDeviation, item => item / AMOUNT_FACTOR); + } else if (valueMetric === TransactionExplorerValueMetric.SourceAmountKurtosis) { + value = kurtosis(allSourceAmountsInDefaultCurrency, averageSourceAmountInDefaultCurrency, variance, item => item / AMOUNT_FACTOR); } } else { value = 0; diff --git a/src/views/desktop/exchangerates/ListPage.vue b/src/views/desktop/exchangerates/ListPage.vue index a612c103..ebdd5d4d 100644 --- a/src/views/desktop/exchangerates/ListPage.vue +++ b/src/views/desktop/exchangerates/ListPage.vue @@ -171,6 +171,7 @@ import { useExchangeRatesPageBase } from '@/views/base/ExchangeRatesPageBase.ts' import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { NumeralSystem } from '@/core/numeral.ts'; +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts'; @@ -302,7 +303,7 @@ function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate, di let exchangeRateAmount: number | '' | null = 0; try { - exchangeRateAmount = getConvertedAmount(baseAmount.value / 100, fromExchangeRate, toExchangeRate); + exchangeRateAmount = getConvertedAmount(baseAmount.value / AMOUNT_FACTOR, fromExchangeRate, toExchangeRate); } catch (ex) { exchangeRateAmount = 0; logger.warn('failed to convert amount by exchange rates, original base amount is ' + baseAmount.value, ex) diff --git a/src/views/mobile/exchangerates/ListPage.vue b/src/views/mobile/exchangerates/ListPage.vue index d52d6183..0d79e9ff 100644 --- a/src/views/mobile/exchangerates/ListPage.vue +++ b/src/views/mobile/exchangerates/ListPage.vue @@ -138,6 +138,7 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { TextDirection } from '@/core/text.ts'; import { NumeralSystem } from '@/core/numeral.ts'; +import { AMOUNT_FACTOR } from '@/consts/numeral.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; import type { LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts'; @@ -277,7 +278,7 @@ function remove(customExchangeRate: LocalizedLatestExchangeRate | null, confirm: function getFinalConvertedAmount(toExchangeRate: LocalizedLatestExchangeRate, displayLocalizedDigits: boolean): string { const fromExchangeRate = exchangeRatesStore.latestExchangeRateMap[baseCurrency.value]; - const exchangeRateAmount = getConvertedAmount(baseAmount.value / 100, fromExchangeRate, toExchangeRate); + const exchangeRateAmount = getConvertedAmount(baseAmount.value / AMOUNT_FACTOR, fromExchangeRate, toExchangeRate); if (!exchangeRateAmount) { if (displayLocalizedDigits) {