DEV Community

Discussion on: Daily Challenge #4 - Checkbook Balancing

Collapse
 
wolverineks profile image
Kevin Sullivan • Edited
// UTILS
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const map = (mapping: {
  (x: any, index: number): string;
  (value: any, index: number, array: any[]): {};
}) => (xs: any[]) => xs.map(mapping);
const sum = (values: number[]) =>
  values.reduce((total, current) => total + current, 0);
const toLines = (text: string) => text.split("\n");
const sort = (sortFunction: (a: any, b: any) => number) => (xs: any[]) =>
  xs.sort(sortFunction);

// DOMAIN
interface Checkbook {
  initialBalance: number;
  checks: Check[];
}
interface Check {
  id: string;
  amount: number;
  type: string;
}
const total = (checks: Check[]) => sum(checks.map(toAmount));
const average = (checks: Check[]) => total(checks) / checks.length;
const toAmount = ({ amount }) => amount;
const currentBalance = (initialBalance: number, checks: Check[]) =>
  initialBalance - total(checks);
const byId = {
  asc: (a: Check, b: Check) => Number(a.id) - Number(b.id),
  desc: (a: Check, b: Check) => Number(b.id) - Number(a.id),
};

// PARSING
const parseId = (text: string) => text;
const parseType = (text: string) =>
  text.replace(/[^a-zA-Z0-9_]/, "").replace(/[!}]/, "");
const parseAmount = (text: string) => parseFloat(text);
const parseCheck = (text: string): Check => {
  const [id, type, amount] = text.split(" ");
  return {
    id: parseId(id),
    type: parseType(type),
    amount: parseAmount(amount),
  };
};
const parse = (text: string) => {
  const [initialBalance, ...checks] = toLines(text);
  return {
    initialBalance: parseAmount(initialBalance),
    checks: checks.map(parseCheck),
  };
};

// HELPERS
const displayAmount = (amount: number) => amount.toFixed(2);
const col = (...children: string[]) => children.join("\n");
// const row = (...children: string[]) => children.join(" ");

// COMPONENTS
const InitialBalance = ({ balance }: { balance: number }) =>
  `Original_Balance: ${displayAmount(balance)}`;
const Check = ({ check }: { check: Check }) =>
  `${check.id} ${check.type} ${displayAmount(check.amount)}`;
const RunningBalance = ({ balance }: { balance: number }) =>
  `Balance ${displayAmount(balance)}`;
const CheckRow = ({ check, balance }) =>
  `${Check({ check })} ${RunningBalance({ balance })}`;
const TotalExpense = ({ total }: { total: number }) =>
  `Total expense ${displayAmount(total)}`;
const AverageExpense = ({ average }: { average: number }) =>
  `Average expense ${displayAmount(average)}`;

// APP
const Checkbook = ({
  checkbook: { initialBalance, checks },
}: {
  checkbook: Checkbook;
}) => {
  const checkRows: (checks: Check[]) => string[] = pipe(
    sort(byId.asc),
    map((check: Check, index: number) =>
      CheckRow({
        check,
        balance: currentBalance(initialBalance, checks.slice(0, index + 1)), // index + 1 means inclusive
      }),
    ),
  );

  return col(
    InitialBalance({ balance: initialBalance }),
    ...checkRows(checks),
    TotalExpense({ total: total(checks) }),
    AverageExpense({ average: average(checks) }),
  );
};

// WRAPPER
export const checkbookBalancer = (text: string) => {
  const checkbook = parse(text);
  return Checkbook({ checkbook });
};