@jjlmoya/utils-finance 1.35.0 → 1.36.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/inflationPurchasingPower/bibliography.astro +14 -0
  7. package/src/tool/inflationPurchasingPower/bibliography.ts +12 -0
  8. package/src/tool/inflationPurchasingPower/component.astro +79 -0
  9. package/src/tool/inflationPurchasingPower/controller.ts +82 -0
  10. package/src/tool/inflationPurchasingPower/dom-views.ts +99 -0
  11. package/src/tool/inflationPurchasingPower/entry.ts +31 -0
  12. package/src/tool/inflationPurchasingPower/evaluator.ts +18 -0
  13. package/src/tool/inflationPurchasingPower/i18n/de.ts +56 -0
  14. package/src/tool/inflationPurchasingPower/i18n/en.ts +131 -0
  15. package/src/tool/inflationPurchasingPower/i18n/es.ts +44 -0
  16. package/src/tool/inflationPurchasingPower/i18n/fr.ts +44 -0
  17. package/src/tool/inflationPurchasingPower/i18n/id.ts +44 -0
  18. package/src/tool/inflationPurchasingPower/i18n/it.ts +44 -0
  19. package/src/tool/inflationPurchasingPower/i18n/ja.ts +44 -0
  20. package/src/tool/inflationPurchasingPower/i18n/ko.ts +44 -0
  21. package/src/tool/inflationPurchasingPower/i18n/nl.ts +44 -0
  22. package/src/tool/inflationPurchasingPower/i18n/pl.ts +44 -0
  23. package/src/tool/inflationPurchasingPower/i18n/pt.ts +44 -0
  24. package/src/tool/inflationPurchasingPower/i18n/ru.ts +44 -0
  25. package/src/tool/inflationPurchasingPower/i18n/sv.ts +44 -0
  26. package/src/tool/inflationPurchasingPower/i18n/tr.ts +44 -0
  27. package/src/tool/inflationPurchasingPower/i18n/zh.ts +44 -0
  28. package/src/tool/inflationPurchasingPower/index.ts +11 -0
  29. package/src/tool/inflationPurchasingPower/inflation-purchasing-power-timeline.css +402 -0
  30. package/src/tool/inflationPurchasingPower/logic.test.ts +35 -0
  31. package/src/tool/inflationPurchasingPower/logic.ts +92 -0
  32. package/src/tool/inflationPurchasingPower/seo.astro +14 -0
  33. package/src/tool/inflationPurchasingPower/storage.ts +29 -0
  34. package/src/tool/inflationPurchasingPower/ui.ts +42 -0
  35. package/src/tools.ts +2 -1
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateInflationTimeline, getMilestoneYears } from './logic';
3
+
4
+ describe('calculateInflationTimeline', () => {
5
+ it('compounds the cost equivalent and discounts unchanged cash', () => {
6
+ const result = calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 5, years: 10, annualCashFlow: 0, cashFlowMode: 'none' });
7
+ expect(result?.priceFactor).toBe(1.63);
8
+ expect(result?.points.at(-1)?.costEquivalent).toBe(1628.89);
9
+ expect(result?.points.at(-1)?.unchangedRealValue).toBe(613.91);
10
+ expect(result?.purchasingPowerLossPercent).toBe(38.61);
11
+ });
12
+
13
+ it('adds annual contributions to the nominal balance and discounts the result', () => {
14
+ const result = calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 2, years: 5, annualCashFlow: 200, cashFlowMode: 'contribution' });
15
+ expect(result?.points.at(-1)?.nominalBalance).toBe(2000);
16
+ expect(result?.points.at(-1)?.realBalance).toBe(1811.46);
17
+ });
18
+
19
+ it('stops a withdrawal scenario at zero and reports depletion', () => {
20
+ const result = calculateInflationTimeline({ startingAmount: 500, annualInflationRate: 2, years: 5, annualCashFlow: 200, cashFlowMode: 'withdrawal' });
21
+ expect(result?.points.at(-1)?.nominalBalance).toBe(0);
22
+ expect(result?.depletionYear).toBe(3);
23
+ });
24
+
25
+ it('deduplicates milestone years for short timelines', () => {
26
+ expect(getMilestoneYears(1)).toEqual([0, 1]);
27
+ expect(getMilestoneYears(4)).toEqual([0, 1, 2, 3, 4]);
28
+ });
29
+
30
+ it('rejects invalid rates, periods and amounts', () => {
31
+ expect(calculateInflationTimeline({ startingAmount: 0, annualInflationRate: 2, years: 5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
32
+ expect(calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: -100, years: 5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
33
+ expect(calculateInflationTimeline({ startingAmount: 1000, annualInflationRate: 2, years: 5.5, annualCashFlow: 0, cashFlowMode: 'none' })).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,92 @@
1
+ export type CashFlowMode = 'none' | 'contribution' | 'withdrawal';
2
+
3
+ export interface InflationPurchasingPowerInput {
4
+ startingAmount: number;
5
+ annualInflationRate: number;
6
+ years: number;
7
+ annualCashFlow: number;
8
+ cashFlowMode: CashFlowMode;
9
+ }
10
+
11
+ export interface InflationTimelinePoint {
12
+ year: number;
13
+ costEquivalent: number;
14
+ unchangedRealValue: number;
15
+ nominalBalance: number;
16
+ realBalance: number;
17
+ }
18
+
19
+ export interface InflationPurchasingPowerResult {
20
+ input: InflationPurchasingPowerInput;
21
+ priceFactor: number;
22
+ purchasingPowerLossPercent: number;
23
+ points: InflationTimelinePoint[];
24
+ milestones: InflationTimelinePoint[];
25
+ depletionYear: number | null;
26
+ }
27
+
28
+ const round = (value: number): number => Number(value.toFixed(2));
29
+
30
+ function hasFiniteValues(input: InflationPurchasingPowerInput): boolean {
31
+ return [input.startingAmount, input.annualInflationRate, input.years, input.annualCashFlow].every(Number.isFinite);
32
+ }
33
+
34
+ function hasValidRanges(input: InflationPurchasingPowerInput): boolean {
35
+ return input.startingAmount > 0
36
+ && input.annualInflationRate > -100
37
+ && input.annualInflationRate <= 100
38
+ && Number.isInteger(input.years)
39
+ && input.years >= 1
40
+ && input.years <= 80
41
+ && input.annualCashFlow >= 0;
42
+ }
43
+
44
+ function hasValidCashFlowMode(input: InflationPurchasingPowerInput): boolean {
45
+ return ['none', 'contribution', 'withdrawal'].includes(input.cashFlowMode);
46
+ }
47
+
48
+ function isValidInput(input: InflationPurchasingPowerInput): boolean {
49
+ return hasFiniteValues(input) && hasValidRanges(input) && hasValidCashFlowMode(input);
50
+ }
51
+
52
+ function getNominalBalance(input: InflationPurchasingPowerInput, year: number): number {
53
+ const flow = input.cashFlowMode === 'contribution' ? input.annualCashFlow : -input.annualCashFlow;
54
+ if (input.cashFlowMode === 'none') return input.startingAmount;
55
+ return Math.max(0, input.startingAmount + flow * year);
56
+ }
57
+
58
+ function createPoint(input: InflationPurchasingPowerInput, year: number): InflationTimelinePoint {
59
+ const priceFactor = (1 + input.annualInflationRate / 100) ** year;
60
+ const nominalBalance = getNominalBalance(input, year);
61
+ return {
62
+ year,
63
+ costEquivalent: round(input.startingAmount * priceFactor),
64
+ unchangedRealValue: round(input.startingAmount / priceFactor),
65
+ nominalBalance: round(nominalBalance),
66
+ realBalance: round(nominalBalance / priceFactor),
67
+ };
68
+ }
69
+
70
+ export function getMilestoneYears(years: number): number[] {
71
+ return [...new Set([0, Math.round(years * 0.25), Math.round(years * 0.5), Math.round(years * 0.75), years])];
72
+ }
73
+
74
+ export function calculateInflationTimeline(input: InflationPurchasingPowerInput): InflationPurchasingPowerResult | null {
75
+ if (!isValidInput(input)) return null;
76
+ const points = Array.from({ length: input.years + 1 }, (_, year) => createPoint(input, year));
77
+ const milestones = getMilestoneYears(input.years)
78
+ .map((year) => points[year])
79
+ .filter((point): point is InflationTimelinePoint => point !== undefined);
80
+ const depletionYear = input.cashFlowMode === 'withdrawal' && input.annualCashFlow > 0
81
+ ? points.find((point) => point.nominalBalance === 0 && point.year > 0)?.year ?? null
82
+ : null;
83
+ const priceFactor = (1 + input.annualInflationRate / 100) ** input.years;
84
+ return {
85
+ input,
86
+ priceFactor: round(priceFactor),
87
+ purchasingPowerLossPercent: round((1 - 1 / priceFactor) * 100),
88
+ points,
89
+ milestones,
90
+ depletionYear,
91
+ };
92
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { inflationPurchasingPower } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await inflationPurchasingPower.i18n[locale]?.();
12
+ ---
13
+
14
+ {content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,29 @@
1
+ import type { InflationPurchasingPowerInput } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-finance-inflation-purchasing-power';
4
+
5
+ function isStoredState(value: Partial<InflationPurchasingPowerInput>): boolean {
6
+ return typeof value.startingAmount === 'number'
7
+ && typeof value.annualInflationRate === 'number'
8
+ && typeof value.years === 'number'
9
+ && typeof value.annualCashFlow === 'number';
10
+ }
11
+
12
+ export function loadInflationPurchasingPowerState(fallback: InflationPurchasingPowerInput): InflationPurchasingPowerInput {
13
+ try {
14
+ const stored = window.localStorage.getItem(STORAGE_KEY);
15
+ if (!stored) return fallback;
16
+ const parsed = JSON.parse(stored) as Partial<InflationPurchasingPowerInput>;
17
+ if (!isStoredState(parsed)) return fallback;
18
+ const cashFlowMode = parsed.cashFlowMode === 'contribution' || parsed.cashFlowMode === 'withdrawal' ? parsed.cashFlowMode : 'none';
19
+ return { ...fallback, ...parsed, cashFlowMode };
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ export function saveInflationPurchasingPowerState(state: InflationPurchasingPowerInput): void {
26
+ try {
27
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
28
+ } catch {}
29
+ }
@@ -0,0 +1,42 @@
1
+ export interface InflationPurchasingPowerUI extends Record<string, string> {
2
+ amountLabel: string;
3
+ amountHelp: string;
4
+ inflationLabel: string;
5
+ inflationHelp: string;
6
+ yearsLabel: string;
7
+ yearsHelp: string;
8
+ cashFlowLabel: string;
9
+ cashFlowHelp: string;
10
+ cashFlowModeLabel: string;
11
+ noCashFlowOption: string;
12
+ contributionOption: string;
13
+ withdrawalOption: string;
14
+ costEquivalentLabel: string;
15
+ realValueLabel: string;
16
+ nominalBalanceLabel: string;
17
+ purchasingPowerLossLabel: string;
18
+ todayLabel: string;
19
+ endLabel: string;
20
+ costLineLabel: string;
21
+ realLineLabel: string;
22
+ balanceLineLabel: string;
23
+ chartDescription: string;
24
+ chartNote: string;
25
+ milestoneTitle: string;
26
+ yearHeader: string;
27
+ costHeader: string;
28
+ realHeader: string;
29
+ balanceHeader: string;
30
+ methodTitle: string;
31
+ methodText: string;
32
+ limitationText: string;
33
+ emptyMessage: string;
34
+ contributionSummary: string;
35
+ withdrawalSummary: string;
36
+ noCashFlowSummary: string;
37
+ depletionWarning: string;
38
+ scenarioNote: string;
39
+ currencyHint: string;
40
+ yearsSuffix: string;
41
+ amountSuffix: string;
42
+ }
package/src/tools.ts CHANGED
@@ -19,5 +19,6 @@ import { PESETA_VALUE_TOOL } from './tool/pesetaValue';
19
19
  import { SALARY_EMPLOYER_COST_TOOL } from './tool/salaryEmployerCost';
20
20
  import { DIVIDEND_YIELD_TOOL } from './tool/dividendYield';
21
21
  import { MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL } from './tool/mortgageExtraPaymentPlanner';
22
+ import { INFLATION_PURCHASING_POWER_TOOL } from './tool/inflationPurchasingPower';
22
23
 
23
- export const ALL_TOOLS: ToolDefinition[] = [COMPOUND_INTEREST_TOOL, FINANCIAL_FREEDOM_TOOL, PROFITABILITY_TOOL, MORTGAGE_TOOL, INFLATION_TOOL, PERCENTAGE_CALCULATOR_TOOL, LATE_INTEREST_TOOL, IBAN_BIC_SWIFT_TOOL, RENT_INCREASE_TOOL, LOTTERY_OPTIMIZER_TOOL, COURT_FEE_CALCULATOR_TOOL, LEGAL_INTEREST_RATE_TOOL, FIRE_CALCULATOR_TOOL, DEBT_SNOWBALL_TOOL, REVOLVING_CARD_CALCULATOR_TOOL, PESETA_VALUE_TOOL, SALARY_EMPLOYER_COST_TOOL, DIVIDEND_YIELD_TOOL, MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL];
24
+ export const ALL_TOOLS: ToolDefinition[] = [COMPOUND_INTEREST_TOOL, FINANCIAL_FREEDOM_TOOL, PROFITABILITY_TOOL, MORTGAGE_TOOL, INFLATION_TOOL, PERCENTAGE_CALCULATOR_TOOL, LATE_INTEREST_TOOL, IBAN_BIC_SWIFT_TOOL, RENT_INCREASE_TOOL, LOTTERY_OPTIMIZER_TOOL, COURT_FEE_CALCULATOR_TOOL, LEGAL_INTEREST_RATE_TOOL, FIRE_CALCULATOR_TOOL, DEBT_SNOWBALL_TOOL, REVOLVING_CARD_CALCULATOR_TOOL, PESETA_VALUE_TOOL, SALARY_EMPLOYER_COST_TOOL, DIVIDEND_YIELD_TOOL, MORTGAGE_EXTRA_PAYMENT_PLANNER_TOOL, INFLATION_PURCHASING_POWER_TOOL];