Skip to content

Commit

Permalink
♻️ (typescript) adding strict typings to utils.ts (actualbudget#2023)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatissJanis authored Dec 6, 2023
1 parent 1972b07 commit 98093a9
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 46 deletions.
38 changes: 24 additions & 14 deletions packages/loot-core/src/mocks/budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,26 @@ import q from '../shared/query';
import type {
CategoryGroupEntity,
PayeeEntity,
TransactionEntity,
NewTransactionEntity,
} from '../types/models';

import random from './random';

function pickRandom(list) {
type MockPayeeEntity = PayeeEntity & { bill?: boolean };

function pickRandom<T>(list: T[]): T {
return list[Math.floor(random() * list.length) % list.length];
}

function number(start, end) {
function number(start: number, end: number) {
return start + (end - start) * random();
}

function integer(start, end) {
function integer(start: number, end: number) {
return Math.round(number(start, end));
}

function findMin(items, field) {
function findMin<T, K extends keyof T>(items: T[], field: K) {
let item = items[0];
for (let i = 0; i < items.length; i++) {
if (items[i][field] < item[field]) {
Expand All @@ -39,17 +41,20 @@ function findMin(items, field) {
return item;
}

function getStartingBalanceCat(categories) {
function getStartingBalanceCat(categories: CategoryGroupEntity[]) {
return categories.find(c => c.name === 'Starting Balances').id;
}

function extractCommonThings(payees, groups) {
function extractCommonThings(
payees: MockPayeeEntity[],
groups: CategoryGroupEntity[],
) {
const incomePayee = payees.find(p => p.name === 'Deposit');
const expensePayees = payees.filter(
p => p.name !== 'Deposit' && p.name !== 'Starting Balance',
);
const expenseGroup = groups.find(g => g.is_income === 0);
const incomeGroup = groups.find(g => g.is_income === 1);
const expenseGroup = groups.find(g => !g.is_income);
const incomeGroup = groups.find(g => g.is_income);
const categories = expenseGroup.categories.filter(
c =>
[
Expand All @@ -73,7 +78,12 @@ function extractCommonThings(payees, groups) {
};
}

async function fillPrimaryChecking(handlers, account, payees, groups) {
async function fillPrimaryChecking(
handlers,
account,
payees: MockPayeeEntity[],
groups: CategoryGroupEntity[],
) {
const {
incomePayee,
expensePayees,
Expand Down Expand Up @@ -107,7 +117,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
amount = integer(0, random() < 0.05 ? -8000 : -700);
}

const transaction: TransactionEntity = {
const transaction: NewTransactionEntity = {
amount,
payee: payee.id,
account: account.id,
Expand All @@ -129,7 +139,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
amount: transaction.amount - a * 2,
category: pick(),
},
] as TransactionEntity[];
];
}
}

Expand Down Expand Up @@ -391,7 +401,7 @@ async function fillOther(handlers, account, payees, groups) {
const numTransactions = integer(3, 6);
const category = incomeGroup.categories.find(c => c.name === 'Income');

const transactions: TransactionEntity[] = [
const transactions: NewTransactionEntity[] = [
{
amount: integer(3250, 3700) * 100 * 100,
payee: payees.find(p => p.name === 'Starting Balance').id,
Expand Down Expand Up @@ -585,7 +595,7 @@ export async function createTestBudget(handlers) {
}),
);

const payees: Array<PayeeEntity & { bill?: boolean }> = [
const payees: Array<MockPayeeEntity> = [
{ name: 'Starting Balance' },
{ name: 'Kroger' },
{ name: 'Publix' },
Expand Down
8 changes: 6 additions & 2 deletions packages/loot-core/src/server/accounts/transaction-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
getApproxNumberThreshold,
} from '../../shared/rules';
import { partitionByField, fastSetMerge } from '../../shared/util';
import { type RuleActionEntity, type RuleEntity } from '../../types/models';
import {
type TransactionEntity,
type RuleActionEntity,
type RuleEntity,
} from '../../types/models';
import { schemaConfig } from '../aql';
import * as db from '../db';
import { getMappings } from '../db/mappings';
Expand Down Expand Up @@ -682,7 +686,7 @@ export async function updateCategoryRules(transactions) {

// Also look 180 days in the future to get any future transactions
// (this might change when we think about scheduled transactions)
let register = await db.all(
let register: TransactionEntity[] = await db.all(
`SELECT t.* FROM v_transactions t
LEFT JOIN accounts a ON a.id = t.account
WHERE date >= ? AND date <= ? AND is_parent = 0 AND a.closed = 0
Expand Down
29 changes: 21 additions & 8 deletions packages/loot-core/src/shared/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { v4 as uuidv4 } from 'uuid';

import { type TransactionEntity } from '../types/models';

import { last, diffItems, applyChanges } from './util';

export function isPreviewId(id) {
Expand Down Expand Up @@ -80,8 +82,8 @@ function getSplit(transactions, parentIndex) {
return split;
}

export function ungroupTransactions(transactions) {
const x = transactions.reduce((list, parent) => {
export function ungroupTransactions(transactions: TransactionEntity[]) {
return transactions.reduce<TransactionEntity[]>((list, parent) => {
const { subtransactions, ...trans } = parent;
const _subtransactions = subtransactions || [];

Expand All @@ -92,25 +94,31 @@ export function ungroupTransactions(transactions) {
}
return list;
}, []);
return x;
}

function groupTransaction(split) {
return { ...split[0], subtransactions: split.slice(1) };
}

export function ungroupTransaction(split) {
export function ungroupTransaction(split: TransactionEntity | null) {
if (split == null) {
return null;
}
return ungroupTransactions([split]);
}

export function applyTransactionDiff(groupedTrans, diff) {
export function applyTransactionDiff(
groupedTrans: Parameters<typeof ungroupTransaction>[0],
diff: Parameters<typeof applyChanges>[0],
) {
return groupTransaction(applyChanges(diff, ungroupTransaction(groupedTrans)));
}

function replaceTransactions(transactions, id, func) {
function replaceTransactions(
transactions: TransactionEntity[],
id: string,
func: (transaction: TransactionEntity) => TransactionEntity,
) {
const idx = transactions.findIndex(t => t.id === id);
const trans = transactions[idx];
const transactionsCopy = [...transactions];
Expand All @@ -127,7 +135,9 @@ function replaceTransactions(transactions, id, func) {
}

const split = getSplit(transactions, parentIndex);
let grouped = func(groupTransaction(split));
let grouped: TransactionEntity | { id: string; _deleted: boolean } = func(
groupTransaction(split),
);
const newSplit = ungroupTransaction(grouped);

let diff;
Expand Down Expand Up @@ -159,7 +169,10 @@ function replaceTransactions(transactions, id, func) {
}
}

export function addSplitTransaction(transactions, id) {
export function addSplitTransaction(
transactions: TransactionEntity[],
id: string,
) {
return replaceTransactions(transactions, id, trans => {
if (!trans.is_parent) {
return trans;
Expand Down
50 changes: 33 additions & 17 deletions packages/loot-core/src/shared/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function last(arr) {
export function last<T>(arr: Array<T>) {
return arr[arr.length - 1];
}

Expand Down Expand Up @@ -33,7 +33,14 @@ export function hasFieldsChanged(obj1, obj2, fields) {
return changed;
}

export function applyChanges(changes, items) {
export function applyChanges<T extends { id: string }>(
changes: {
added?: T[];
updated?: T[];
deleted?: T[];
},
items: T[],
) {
items = [...items];

if (changes.added) {
Expand Down Expand Up @@ -64,7 +71,7 @@ export function applyChanges(changes, items) {
return items;
}

export function partitionByField(data, field) {
export function partitionByField<T, K extends keyof T>(data: T[], field: K) {
const res = new Map();
for (let i = 0; i < data.length; i++) {
const item = data[i];
Expand Down Expand Up @@ -93,7 +100,7 @@ export function groupBy<T, K extends keyof T>(data: T[], field: K) {
// `Map` is better, but we can't swap it out because `Map` has a
// different API and we need to go through and update everywhere that
// uses it.
function _groupById(data) {
function _groupById<T extends { id: string }>(data: T[]) {
const res = new Map();
for (let i = 0; i < data.length; i++) {
const item = data[i];
Expand All @@ -102,7 +109,7 @@ function _groupById(data) {
return res;
}

export function diffItems(items, newItems) {
export function diffItems<T extends { id: string }>(items: T[], newItems: T[]) {
const grouped = _groupById(items);
const newGrouped = _groupById(newItems);
const added = [];
Expand All @@ -127,7 +134,7 @@ export function diffItems(items, newItems) {
return { added, updated, deleted };
}

export function groupById(data) {
export function groupById<T extends { id: string }>(data: T[]) {
const res = {};
for (let i = 0; i < data.length; i++) {
const item = data[i];
Expand Down Expand Up @@ -168,7 +175,7 @@ export function getIn(map, keys) {
return item;
}

export function fastSetMerge(set1, set2) {
export function fastSetMerge<T>(set1: Set<T>, set2: Set<T>) {
const finalSet = new Set(set1);
const iter = set2.values();
let value = iter.next();
Expand Down Expand Up @@ -214,7 +221,13 @@ export function setNumberFormat(config: typeof numberFormatConfig) {
numberFormatConfig = config;
}

export function getNumberFormat({ format, hideFraction } = numberFormatConfig) {
export function getNumberFormat({
format,
hideFraction,
}: {
format: NumberFormats;
hideFraction: boolean;
} = numberFormatConfig) {
let locale, regex, separator;

switch (format) {
Expand Down Expand Up @@ -282,19 +295,22 @@ export function safeNumber(value: number) {
return value;
}

export function toRelaxedNumber(value) {
export function toRelaxedNumber(value: string) {
return integerToAmount(currencyToInteger(value) || 0);
}

export function integerToCurrency(n, formatter = getNumberFormat().formatter) {
export function integerToCurrency(
n: number,
formatter = getNumberFormat().formatter,
) {
return formatter.format(safeNumber(n) / 100);
}

export function amountToCurrency(n) {
return getNumberFormat().formatter.format(n);
}

export function currencyToAmount(str) {
export function currencyToAmount(str: string) {
const amount = parseFloat(
str
.replace(getNumberFormat().regex, '')
Expand All @@ -303,20 +319,20 @@ export function currencyToAmount(str) {
return isNaN(amount) ? null : amount;
}

export function currencyToInteger(str) {
export function currencyToInteger(str: string) {
const amount = currencyToAmount(str);
return amount == null ? null : amountToInteger(amount);
}

export function stringToInteger(str) {
export function stringToInteger(str: string) {
const amount = parseInt(str.replace(/[^-0-9.,]/g, ''));
if (!isNaN(amount)) {
return amount;
}
return null;
}

export function amountToInteger(n) {
export function amountToInteger(n: number) {
return Math.round(n * 100);
}

Expand All @@ -328,12 +344,12 @@ export function integerToAmount(n) {
// financial files and we don't want to parse based on the user's
// number format, because the user could be importing from many
// currencies. We extract out the numbers and just ignore separators.
export function looselyParseAmount(amount) {
function safeNumber(v) {
export function looselyParseAmount(amount: string) {
function safeNumber(v: number): null | number {
return isNaN(v) ? null : v;
}

function extractNumbers(v) {
function extractNumbers(v: string): string {
return v.replace(/[^0-9-]/g, '');
}

Expand Down
18 changes: 13 additions & 5 deletions packages/loot-core/src/types/models/transaction.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import type { CategoryEntity } from './category';
import type { PayeeEntity } from './payee';
import type { ScheduleEntity } from './schedule';

export interface TransactionEntity {
id?: string;
export interface NewTransactionEntity {
is_parent?: boolean;
is_child?: boolean;
parent_id?: string;
account: AccountEntity;
category?: CategoryEntity;
account: string;
category?: string;
amount: number;
payee?: PayeeEntity;
payee?: string;
notes?: string;
date: string;
imported_id?: string;
Expand All @@ -22,6 +21,15 @@ export interface TransactionEntity {
cleared?: boolean;
reconciled?: boolean;
tombstone?: boolean;
schedule?: string;
subtransactions?: Omit<NewTransactionEntity, 'account' | 'date'>[];
}

export interface TransactionEntity extends NewTransactionEntity {
id: string;
account: AccountEntity;
category?: CategoryEntity;
payee?: PayeeEntity;
schedule?: ScheduleEntity;
subtransactions?: TransactionEntity[];
}
6 changes: 6 additions & 0 deletions upcoming-release-notes/2023.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---

Added more strict typings to `utils.ts` and some of its dependencies

0 comments on commit 98093a9

Please sign in to comment.