Expense Data and Array Methods
With the scaffolding in place, it is time to implement the first two real modules: expense.js (the data model, already written in Module 02) and storage.js (localStorage persistence). This lesson also builds the helper functions that process the expenses array using the methods from Module 01.
expense.js — the final version
Section titled “expense.js — the final version”Copy the complete Expense class from Module 02 into expense.js:
export default class Expense { static CATEGORIES = ['Food', 'Transport', 'Health', 'Entertainment', 'Other'];
#id; #createdAt;
constructor({ id, description, amount, category, date, createdAt } = {}) { if (!description?.trim()) throw new Error('Description is required'); if (typeof amount !== 'number' || amount <= 0) throw new Error('Amount must be positive');
this.#id = id ?? crypto.randomUUID(); this.#createdAt = createdAt ?? Date.now(); this.description = description.trim(); this.amount = amount; this.category = category ?? 'Other'; this.date = date ?? new Date().toISOString().split('T')[0]; }
get id() { return this.#id; } get createdAt() { return this.#createdAt; }
format() { return `${this.description}: $${this.amount.toFixed(2)}`; } isInCategory(c) { return this.category.toLowerCase() === c.toLowerCase(); }
toJSON() { return { id: this.#id, description: this.description, amount: this.amount, category: this.category, date: this.date, createdAt: this.#createdAt, }; }
static fromJSON(data) { return new Expense(data); } static isValidCategory(c) { return Expense.CATEGORIES.includes(c); }}storage.js — localStorage persistence
Section titled “storage.js — localStorage persistence”import Expense from './expense.js';
const STORAGE_KEY = 'budgetbuddy_expenses';
export function saveExpenses(expenses) { localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses.map(e => e.toJSON())));}
export function loadExpenses() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; try { return JSON.parse(raw).map(data => Expense.fromJSON(data)); } catch { return []; }}toJSON() serializes to a plain object (private fields included). fromJSON() reconstructs the instance. The try/catch handles corrupted localStorage data — return an empty array rather than crashing.
Array method helpers
Section titled “Array method helpers”These are the pure functions that process the expenses array. They use only the array methods from Module 01 and take the expenses array as a parameter — no global state, no side effects:
// In main.js or a separate data.js if you prefer
export function filterByCategory(expenses, category) { if (category === 'All') return expenses; return expenses.filter(e => e.isInCategory(category));}
export function sortByDate(expenses) { return [...expenses].sort((a, b) => new Date(b.date) - new Date(a.date));}
export function getTotal(expenses) { return expenses.reduce((acc, e) => acc + e.amount, 0);}
export function getTotalByCategory(expenses) { return expenses.reduce((acc, e) => { acc[e.category] = (acc[e.category] ?? 0) + e.amount; return acc; }, {});}These stay in main.js for now. They are short enough that a separate file is not worth the overhead.
Wiring storage in main.js
Section titled “Wiring storage in main.js”Update main.js to actually load and save expenses:
import Expense from './expense.js';import { saveExpenses, loadExpenses } from './storage.js';import { fetchRates, convertAmount } from './api.js';import { renderExpenses, updateTotal } from './ui.js';
let expenses = loadExpenses();
function filterByCategory(list, category) { return category === 'All' ? list : list.filter(e => e.isInCategory(category));}
function sortByDate(list) { return [...list].sort((a, b) => new Date(b.date) - new Date(a.date));}
function getTotal(list) { return list.reduce((acc, e) => acc + e.amount, 0);}
function getDisplayList(category = 'All') { return sortByDate(filterByCategory(expenses, category));}
console.log('Loaded expenses:', expenses.length);Reload. If you have any expenses in localStorage from previous testing, expenses.length will be positive. If not, it is 0. Either way — no errors.
Exercise
Section titled “Exercise”- Implement
expense.jswith the fullExpenseclass. - Implement
storage.jswithsaveExpensesandloadExpenses. - In the browser console, manually test the round-trip:
// After the page loads, in the console:const e = new Expense({ description: 'Test', amount: 10, category: 'Food' });saveExpenses([e]);const loaded = loadExpenses();console.log(loaded[0].format()); // 'Test: $10.00'
- Add the four helper functions (
filterByCategory,sortByDate,getTotal,getTotalByCategory) tomain.js. - Log
getTotalByCategory(expenses)— should be{}on first load, or populated if you saved test data.
expense.jscontains the completeExpenseclass with private fields, validation, and JSON round-trip methods.storage.jsserializes instances withtoJSON()and reconstructs them withfromJSON().- Array helper functions take the expenses array as a parameter and return new arrays — no mutation, no side effects.
sortByDatespreads before sorting to protect the original array.- Test the data layer in isolation before building the UI on top of it.