diff --git a/finance-tracker/.gitignore b/finance-tracker/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/finance-tracker/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/finance-tracker/.prettierrc b/finance-tracker/.prettierrc new file mode 100644 index 0000000..5fb6e1a --- /dev/null +++ b/finance-tracker/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2 +} diff --git a/finance-tracker/app.js b/finance-tracker/app.js index 7cfcd07..8c1068d 100644 --- a/finance-tracker/app.js +++ b/finance-tracker/app.js @@ -1,3 +1,45 @@ // This is the entrypoint for your application. // node app.js +// app.js +import {printAllTransactions, addTransaction, getTransactionsByCategory, getTransactionsByDateRange, groupTransactionsByMonth, getAverageExpensePerCategory, removeTransactionById, findConsecutiveExpensiveMonths,} from "./finance.js"; +// Print data +printAllTransactions(); + +// Add one transaction +addTransaction({ + id: 6, + type: "expense", + category: "transport", + amount: 60, + description: "Train ticket", + date: "2026-02-02", +}); + +console.log("\nAfter adding one transaction:\n"); +printAllTransactions(); + +// Filter by category +const foodTransactions = getTransactionsByCategory("food"); +console.log("\nFood transactions:", foodTransactions); + +//Bonus Challenges +// Search transactions by date range using slice +const februaryRange = getTransactionsByDateRange("2026-02-02", "2026-02-04"); +console.log("\nDate range 2026-02-02..2026-02-04:", februaryRange); + +// Group transactions by month using nested objects +const groupedByMonth = groupTransactionsByMonth(); +console.log("\nGrouped by month:", groupedByMonth); + +// Calculate average expense per category +const average = getAverageExpensePerCategory(); +console.log("\nAverage expense per category:", average); + +// Add ability to remove transactions by id +const removedById = removeTransactionById(2); +console.log("\nRemoved transaction with id=2:", removedById); + +// Create a function that finds consecutive expensive months +const sequences = findConsecutiveExpensiveMonths(500); +console.log("\nConsecutive expensive months (>= 500):", sequences); \ No newline at end of file diff --git a/finance-tracker/data.js b/finance-tracker/data.js index d7863ff..8004dd4 100644 --- a/finance-tracker/data.js +++ b/finance-tracker/data.js @@ -1,2 +1,43 @@ // Place here the transaction data array. Use it in your application as needed. -const transactions = []; \ No newline at end of file +export const transactions = [ + { + id: 1, + type: "income", + category: "salary", + amount: 3000, + description: "Monthly salary", + date: "2026-01-26", + }, + { + id: 2, + type: "expense", + category: "food", + amount: 400, + description: "Groceries", + date: "2026-01-28", + }, + { + id: 3, + type: "expense", + category: "housing", + amount: 1300, + description: "Rent", + date: "2026-01-27", + }, + { + id: 4, + type: "income", + category: "side-income", + amount: 600, + description: "Freelance", + date: "2026-01-29", + }, + { + id: 5, + type: "expense", + category: "bills", + amount: 250, + description: "Utilities", + date: "2026-01-30", + }, +]; diff --git a/finance-tracker/finance.js b/finance-tracker/finance.js index ac2118f..7eb259b 100644 --- a/finance-tracker/finance.js +++ b/finance-tracker/finance.js @@ -1,27 +1,258 @@ -function addTransaction(transaction) { - // TODO: Implement this function +// finance.js +import chalk from "chalk"; +import { transactions } from "./data.js"; + +// Format number +function formatEUR(amount) { + return `€${amount}`; +} + +/* + 1) addTransaction - Add new transaction to array + - Uses destructuring + - Uses spread operator when pushing +*/ +export function addTransaction(transaction) { + const { id, type, category, amount, description, date } = transaction; + if ( id == null || !type || !category || amount == null || !description || !date ) { + console.log(chalk.red("❌ Missing required fields")); + return false; + } + transactions.push({ ...transaction }); + return true; +} + +/* + 2) getTotalIncome() + - Sum income using a loop +*/ +export function getTotalIncome() { + let sum = 0; + for (const transaction of transactions) { + if (transaction.type === "income") { + sum += transaction.amount; + } + } + return sum; +} + +/* + 3) getTotalExpenses() + - Sum expenses using a loop +*/ +export function getTotalExpenses() { + let sum = 0; + for (const transaction of transactions) { + if (transaction.type === "expense") { + sum = sum + transaction.amount; + } + } + return sum; +} + +/* + 4) getBalance() - Calculate total income minus expenses +*/ +export function getBalance() { + return getTotalIncome() - getTotalExpenses(); } -function getTotalIncome() { - // TODO: Implement this function +/* + 5) getTransactionsByCategory(category) - Filter transactions + - Loop + push +*/ +export function getTransactionsByCategory(category) { + const result = []; + const target = category.toLowerCase(); + for (const transaction of transactions) { + if (transaction.category.toLowerCase() === target) { + result.push(transaction); + } + } + return result; } -function getTotalExpenses() { - // TODO: Implement this function +/* + 6) getLargestExpense() - Find highest expense amount +*/ +export function getLargestExpense() { + let largest = null; + for (const transaction of transactions) { + if (transaction.type === "expense") { + if (largest === null || transaction.amount > largest.amount) { + largest = transaction; + } + } + } + return largest; } -function getBalance() { - // TODO: Implement this function +/* + 7) printAllTransactions() - Display all transactions with formatting +*/ +export function printAllTransactions() { + console.log(chalk.bold("💰 PERSONAL FINANCE TRACKER 💰")); + console.log(""); + console.log(chalk.bold("All Transactions:")); + let i = 1; + for (const transaction of transactions) { + const { type, category, amount, description } = transaction; + const typeLabel = type.toUpperCase(); + const categoryColored = chalk.yellow(category); + const amountText = formatEUR(amount); + const amountColored = + type === "income" ? chalk.green(amountText) : chalk.red(amountText); + console.log( + `${i}. [${typeLabel}] ${description} - ${amountColored} (${categoryColored})` + ); + + i = i + 1; + } + const totalIncome = getTotalIncome(); + const totalExpenses = getTotalExpenses(); + const balance = getBalance(); + const count = transactions.length; + const largestExpense = getLargestExpense(); + console.log(""); + console.log(chalk.bold("📊 FINANCIAL SUMMARY 📊")); + + console.log(chalk.bold(`Total Income: ${chalk.green(formatEUR(totalIncome))}`)); + console.log( + chalk.bold(`Total Expenses: ${chalk.red(formatEUR(totalExpenses))}`) + ); + const balanceColored = + balance >= 0 ? chalk.cyan(formatEUR(balance)) : chalk.red(formatEUR(balance)); + console.log(chalk.bold(`Current Balance: ${balanceColored}`)); + console.log(chalk.bold(`Total Transactions: ${count}`)); + if (largestExpense) { + console.log( + chalk.bold( + `Largest Expense: ${largestExpense.description} (${chalk.red( + formatEUR(largestExpense.amount) + )})` + ) + ); + } else { + console.log(chalk.bold("Largest Expense: none")); + } +} + +/* Bonus Challenges*/ + +/* + Bonus 1: Search transactions by date range using slice + - Copy + sort by date + - Find start and end indices + - Return slice(startIndex, endIndex+1) +*/ +export function getTransactionsByDateRange(startDate, endDate) { + const sorted = [...transactions].sort((a, b) => a.date.localeCompare(b.date)); + let startIndex = -1; + let endIndex = -1; + // Find first index with date >= startDate + for (let i = 0; i < sorted.length; i++) { + if (sorted[i].date >= startDate) { + startIndex = i; + break; + } + } + // Find last index with date <= endDate + for (let i = sorted.length - 1; i >= 0; i--) { + if (sorted[i].date <= endDate) { + endIndex = i; + break; + } + } + if (startIndex === -1 || endIndex === -1 || startIndex > endIndex) { + return []; + } + return sorted.slice(startIndex, endIndex + 1); } -function getTransactionsByCategory(category) { - // TODO: Implement this function +/* + Bonus 2: Group transactions by month using nested objects +*/ +export function groupTransactionsByMonth() { + const groups = {}; + for (const transaction of transactions) { + const month = transaction.date.slice(0, 7); // "YYYY-MM" + if (!groups[month]) { + groups[month] = { income: [], expense: [] }; + } + if (transaction.type === "income") groups[month].income.push(transaction); + if (transaction.type === "expense") groups[month].expense.push(transaction); + } + return groups; } -function getLargestExpense() { - // TODO: Implement this function +/* + Bonus 3: Calculate average expense per category +*/ +export function getAverageExpensePerCategory() { + const categorySumCount = {}; + for (const transaction of transactions) { + if (transaction.type !== "expense") continue; + const c = transaction.category; + if (!categorySumCount[c]) categorySumCount[c] = { sum: 0, count: 0 }; + categorySumCount[c].sum = categorySumCount[c].sum + transaction.amount; + categorySumCount[c].count = categorySumCount[c].count + 1; + } + const averages = {}; + for (const category of Object.keys(categorySumCount)) { + averages[category] = categorySumCount[category].sum / categorySumCount[category].count; + } + return averages; } -function printAllTransactions() { - // TODO: Implement this function -} \ No newline at end of file +/* + Bonus 4: Remove transaction by id + +*/ +export function removeTransactionById(id) { + for (let i = 0; i < transactions.length; i++) { + if (transactions[i].id === id) { + const removed = transactions.splice(i, 1); // returns array of removed items + return removed[0]; + } + } + return null; +} + +/* + Bonus 5: Create a function that finds consecutive expensive months +*/ +export function findConsecutiveExpensiveMonths(threshold) { + const groups = groupTransactionsByMonth(); + const months = Object.keys(groups).sort(); // chronological order + // Build monthly expense totals + const monthlyExpense = {}; + for (const month of months) { + let sum = 0; + for (const transaction of groups[month].expense) { + sum = sum + transaction.amount; + } + monthlyExpense[month] = sum; + } + const sequences = []; + let i = 0; + // while loop + while (i < months.length) { + const month = months[i]; + if (monthlyExpense[month] >= threshold) { + const seq = [month]; + i = i + 1; + // Keep going while next months are also expensive + while (i < months.length && monthlyExpense[months[i]] >= threshold) { + seq.push(months[i]); + i = i + 1; + } + // Save only if sequence has at least 2 months + if (seq.length >= 2) { + sequences.push(seq); + } + } else { + i = i + 1; + } + } + return sequences; +} diff --git a/finance-tracker/package-lock.json b/finance-tracker/package-lock.json new file mode 100644 index 0000000..2195e5f --- /dev/null +++ b/finance-tracker/package-lock.json @@ -0,0 +1,105 @@ +{ + "name": "finance-tracker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "finance-tracker", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^4.1.2" + }, + "devDependencies": { + "prettier": "^3.8.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/finance-tracker/package.json b/finance-tracker/package.json new file mode 100644 index 0000000..5b85e7c --- /dev/null +++ b/finance-tracker/package.json @@ -0,0 +1,16 @@ +{ + "name": "finance-tracker", + "version": "1.0.0", + "description": "", + "type": "module", + "scripts": { + "start": "node app.js", + "format": "prettier --write ." + }, + "dependencies": { + "chalk": "4.1.2" + }, + "devDependencies": { + "prettier": "^3.8.1" + } +}