Skip to content

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.

Copy the complete Expense class from Module 02 into expense.js:

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
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.

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.

Update main.js to actually load and save expenses:

main.js
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.

  1. Implement expense.js with the full Expense class.
  2. Implement storage.js with saveExpenses and loadExpenses.
  3. 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'
  4. Add the four helper functions (filterByCategory, sortByDate, getTotal, getTotalByCategory) to main.js.
  5. Log getTotalByCategory(expenses) — should be {} on first load, or populated if you saved test data.
  • expense.js contains the complete Expense class with private fields, validation, and JSON round-trip methods.
  • storage.js serializes instances with toJSON() and reconstructs them with fromJSON().
  • Array helper functions take the expenses array as a parameter and return new arrays — no mutation, no side effects.
  • sortByDate spreads before sorting to protect the original array.
  • Test the data layer in isolation before building the UI on top of it.