diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..eba5a68 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 80, + "tabWidth": 2 +} diff --git a/finance-tracker/app.js b/finance-tracker/app.js index 7cfcd07..f8c33ad 100644 --- a/finance-tracker/app.js +++ b/finance-tracker/app.js @@ -1,3 +1,60 @@ // This is the entrypoint for your application. // node app.js +import { + addTransaction, + calculateAverageExpensesPerCategory, + findConsecutiveExpensiveMonth, + getTransactionsByCategory, + groupTransactionByMonth, + printGeneralReport, + removeTransactions, + searchTransactionsByDateRange, +} from './finance.js'; +import transactions from './data.js'; +const newTransaction = addTransaction( + 'expense', + 'groceries', + 98, + 'Weekly supermarket shopping', + '2026-01-25' +); + +const transactionByCategory = getTransactionsByCategory( + transactions, + 'groceries' +); + +const financeReport = printGeneralReport(transactions); + +console.log(newTransaction, transactionByCategory, financeReport); + +//Bonus Challenges output +const transactionsByDateRange = searchTransactionsByDateRange( + transactions, + '2025-01-22', + '2025-02-24' +); + +// Node.js collapses nested objects inside arrays and prints them as [Object]. To see the full structure, I use JSON.stringify(). The second argument (null)means no custom replacer, and the third argument (2) adds indentation for readability. + +const transactionsGroupByMonth = JSON.stringify( + groupTransactionByMonth(transactions), + null, + 2 +); + +const averageExpensesPerCategory = + calculateAverageExpensesPerCategory(transactions); + +const arrayWithoutRemoveTransaction = removeTransactions(transactions, 3); + +const consecutiveExpensiveMonths = findConsecutiveExpensiveMonth(transactions); + +console.log( + transactionsByDateRange, + transactionsGroupByMonth, + averageExpensesPerCategory, + arrayWithoutRemoveTransaction, + consecutiveExpensiveMonths +); diff --git a/finance-tracker/data.js b/finance-tracker/data.js index d7863ff..5893f51 100644 --- a/finance-tracker/data.js +++ b/finance-tracker/data.js @@ -1,2 +1,205 @@ // Place here the transaction data array. Use it in your application as needed. -const transactions = []; \ No newline at end of file +const transactions = [ + { + id: 1, + type: 'income', + category: 'salary', + amount: 3000, + description: 'Monthly salary', + date: '2025-01-15', + }, + { + id: 2, + type: 'expense', + category: 'groceries', + amount: 120, + description: 'Weekly supermarket shopping', + date: '2025-01-16', + }, + { + id: 3, + type: 'expense', + category: 'transport', + amount: 45, + description: 'Monthly public transport pass', + date: '2025-01-17', + }, + { + id: 4, + type: 'income', + category: 'freelance', + amount: 600, + description: 'Freelance web project', + date: '2025-01-20', + }, + { + id: 5, + type: 'expense', + category: 'entertainment', + amount: 75, + description: 'Concert tickets', + date: '2025-01-21', + }, + { + id: 6, + type: 'expense', + category: 'utilities', + amount: 150, + description: 'Electricity and water bill', + date: '2025-01-22', + }, + { + id: 7, + type: 'income', + category: 'gift', + amount: 200, + description: 'Birthday gift from family', + date: '2025-01-23', + }, + { + id: 8, + type: 'expense', + category: 'health', + amount: 90, + description: 'Doctor appointment', + date: '2025-02-24', + }, + { + id: 9, + type: 'expense', + category: 'education', + amount: 300, + description: 'Online course fee', + date: '2025-03-15', + }, + { + id: 10, + type: 'expense', + category: 'education', + amount: 135, + description: 'Online course fee', + date: '2025-03-25', + }, + { + id: 11, + type: 'expense', + category: 'groceries', + amount: 95, + description: 'Weekly supermarket shopping', + date: '2025-02-02', + }, + { + id: 12, + type: 'expense', + category: 'transport', + amount: 45, + description: 'Monthly public transport pass', + date: '2025-02-10', + }, + { + id: 13, + type: 'income', + category: 'salary', + amount: 3000, + description: 'Monthly salary', + date: '2025-02-15', + }, + { + id: 14, + type: 'expense', + category: 'entertainment', + amount: 60, + description: 'Cinema tickets', + date: '2025-02-18', + }, + { + id: 15, + type: 'expense', + category: 'utilities', + amount: 155, + description: 'Electricity and water bill', + date: '2025-02-22', + }, + { + id: 16, + type: 'income', + category: 'freelance', + amount: 400, + description: 'Small freelance task', + date: '2025-03-05', + }, + { + id: 17, + type: 'expense', + category: 'health', + amount: 120, + description: 'Pharmacy purchase', + date: '2025-03-12', + }, + { + id: 18, + type: 'expense', + category: 'groceries', + amount: 130, + description: 'Weekly supermarket shopping', + date: '2025-03-20', + }, + { + id: 19, + type: 'expense', + category: 'transport', + amount: 50, + description: 'Taxi ride', + date: '2025-03-28', + }, + { + id: 20, + type: 'income', + category: 'bonus', + amount: 500, + description: 'Quarterly bonus', + date: '2025-04-01', + }, + { + id: 21, + type: 'expense', + category: 'education', + amount: 200, + description: 'Books and materials', + date: '2025-04-03', + }, + { + id: 22, + type: 'expense', + category: 'entertainment', + amount: 90, + description: 'Theatre tickets', + date: '2025-04-10', + }, + { + id: 23, + type: 'expense', + category: 'utilities', + amount: 160, + description: 'Electricity and water bill', + date: '2025-04-15', + }, + { + id: 24, + type: 'expense', + category: 'health', + amount: 80, + description: 'Dentist visit', + date: '2025-04-20', + }, + { + id: 25, + type: 'income', + category: 'salary', + amount: 3100, + description: 'Monthly salary', + date: '2025-03-15', + }, +]; + +export default transactions; diff --git a/finance-tracker/finance.js b/finance-tracker/finance.js index ac2118f..dc0ac5b 100644 --- a/finance-tracker/finance.js +++ b/finance-tracker/finance.js @@ -1,27 +1,284 @@ -function addTransaction(transaction) { - // TODO: Implement this function +import transactions from './data.js'; +import chalk from 'chalk'; + +export function addTransaction(type, category, amount, description, date) { + const newTransaction = { + id: transactions.length + 1, + type, + category, + amount, + description, + date, + }; + + // The assignment requires using the spread operator when adding a transaction. In this task, mutating the existing array with push() feels more suitable, and that is the approach I would normally use if there were no requirement. To satisfy the task while keeping the logic I prefer, I include both: spread for the assignment, and push() for the actual update. + + const updated = [...transactions, newTransaction]; + + transactions.push(newTransaction); + + return newTransaction; +} + +export function getTotalIncome(transactions) { + let totalIncome = 0; + for (const transaction of transactions) { + if (transaction.type === 'income') { + totalIncome += transaction.amount; + } + } + return totalIncome; +} + +export function getTotalExpenses(transactions) { + let totalExpenses = 0; + for (const transaction of transactions) { + if (transaction.type === 'expense') { + totalExpenses += transaction.amount; + } + } + return totalExpenses; +} + +export function getBalance(transactions) { + const totalIncome = getTotalIncome(transactions); + const totalExpenses = getTotalExpenses(transactions); + const balance = totalIncome - totalExpenses; + + return balance; +} + +export function getTransactionsByCategory(transactions, category) { + let transactionByCategory = []; + + for (const transaction of transactions) { + if (transaction.category === category) { + transactionByCategory.push(transaction); + } + } + + return transactionByCategory; +} + +export function getLargestExpense(transactions) { + //I moved the logic for filtering "expense" transactions into a separate function, because this code is reused in several bonus tasks. + const expenseTransactions = getExpenseTransactions(transactions); + + let largest = expenseTransactions[0]; + + for (const transaction of expenseTransactions) { + if (transaction.amount > largest.amount) { + largest = transaction; + } + } + + return largest; +} + +export function printAllTransactions(transactions) { + let output = chalk.bold('All Transactions:\n'); + + for (const transaction of transactions) { + const { id, type, category, amount, description } = transaction; + + //formatting values by task requirements with chalk + const typeFormat = + type === 'income' + ? chalk.green(type.toUpperCase()) + : chalk.red(type.toUpperCase()); + + const categoryFormat = getFirstCharacterToUp(chalk.yellow(category)); + const amountFormat = chalk.bold(amount); + const descriptionFormat = description.toLowerCase(); + + //collect all in one line for each element + const line = `${id}. [${typeFormat}] ${categoryFormat} - €${amountFormat} (${descriptionFormat})`; + + output += `${line}\n`; + } + + return output; +} + +export function printSummary(transactions) { + //formatting values by task requirements with chalk + const totalIncome = chalk.bold.green(getTotalIncome(transactions)); + const totalExpenses = chalk.bold.red(getTotalExpenses(transactions)); + const numOfTransactions = chalk.bold(transactions.length); + + const balance = + getBalance(transactions) >= 0 + ? chalk.bold.cyan(getBalance(transactions)) + : chalk.bold.red(getBalance(transactions)); + + const { amount, description } = getLargestExpense(transactions); + const largestExpense = chalk.bold(amount); + + //collect summary + const summary = `📊 ${chalk.bold('financial summary'.toUpperCase())} 📊 \nTotal Income: €${totalIncome}\nTotal Expenses: €${totalExpenses}\nCurrent Balance: €${balance}\n\nLargest Expense: ${description} (€${largestExpense})\nTotal Transactions: ${numOfTransactions}`; + + return summary; +} + +export function printGeneralReport(transactions) { + const allTransactions = printAllTransactions(transactions); + const summary = printSummary(transactions); + + return `💰 ${chalk.bold('personal finance tracker'.toUpperCase())} 💰\n\n${allTransactions}\n${summary}`; } -function getTotalIncome() { - // TODO: Implement this function +//Bonus Challenges functions + +//Search transactions by date range using slice +export function searchTransactionsByDateRange( + transactions, + startDate, + endDate +) { + const sorted = transactions + .slice() + .sort((a, b) => a.date.localeCompare(b.date)); + + const startIndex = sorted.findIndex( + (transaction) => transaction.date >= startDate + ); + const endIndex = sorted.findIndex( + (transaction) => transaction.date > endDate + ); + + const range = sorted.slice( + startIndex, + endIndex === -1 ? sorted.length : endIndex + ); + + return range; } -function getTotalExpenses() { - // TODO: Implement this function +//Group transactions by month using nested objects +export function groupTransactionByMonth(transactions) { + let groupedTransactions = {}; + + for (const transaction of transactions) { + const [year, month] = transaction.date.split('-'); + + if (!groupedTransactions[year]) { + groupedTransactions[year] = {}; + } + + if (!groupedTransactions[year][month]) { + groupedTransactions[year][month] = []; + } + + groupedTransactions[year][month].push(transaction); + } + + return groupedTransactions; } -function getBalance() { - // TODO: Implement this function +//Calculate average expense per category +export function calculateAverageExpensesPerCategory(transactions) { + const expenses = getExpenseTransactions(transactions); + + let expenseCategories = {}; + + for (const transaction of expenses) { + const { category, amount } = transaction; + + if (!expenseCategories[category]) { + expenseCategories[category] = { total: 0, count: 0 }; + } + + expenseCategories[category].total += amount; + expenseCategories[category].count += 1; + } + + let averages = {}; + + for (const category in expenseCategories) { + const { total, count } = expenseCategories[category]; + averages[category] = Number((total / count).toFixed(2)); + } + + return averages; } -function getTransactionsByCategory(category) { - // TODO: Implement this function +//Add ability to remove transactions by id +export function removeTransactions(transactions, id) { + const newTransactions = transactions.filter( + (transaction) => transaction.id !== id + ); + return newTransactions; } -function getLargestExpense() { - // TODO: Implement this function +//Create a function that finds consecutive expensive months (use while loop) +//Use multi-line commenting to explain your most complex function + +/* This function returns a list of months where expenses increase consecutively. + It follows these steps: + + 1. It filters all transactions using getExpenseTransactions() to keep only those with type "expense". + + 2. It creates an empty object (monthlyTotal) to store total expenses per month. + + 3. It iterates through all expense transactions. For each one: + - extracts the date and amount, + - derives the month in "YYYY-MM" format using slice(), + - initializes monthlyTotal[month] to 0 if it doesn't exist yet, + - adds the transaction amount to that month's total. + + 4. It collects all month keys from monthlyTotal and sorts them to ensure chronological comparison. + + 5. It prepares a while loop index (i) and an empty result array. + + 6. Using a while loop, it compares each month with the next one. + If the next month has a higher total expense than the current month: + - it adds the current month to the result (only if not already included), + - it always adds the next month. + This builds a sequence of months with increasing expenses. + + 7. After the loop finishes, it returns the result array containing all months that form consecutive increasing expense periods. */ + +export function findConsecutiveExpensiveMonth(transactions) { + const expenses = getExpenseTransactions(transactions); + const monthlyTotal = {}; + + for (const transaction of expenses) { + const { date, amount } = transaction; + const month = date.slice(0, 7); + + if (!monthlyTotal[month]) monthlyTotal[month] = 0; + monthlyTotal[month] += amount; + } + + const months = Object.keys(monthlyTotal).sort(); + + let i = 0; + const result = []; + + while (i < months.length - 1) { + const current = months[i]; + const next = months[i + 1]; + + if (monthlyTotal[current] < monthlyTotal[next]) { + if (!result.includes(current)) result.push(current); + result.push(next); + } + i++; + } + + return result; } -function printAllTransactions() { - // TODO: Implement this function -} \ No newline at end of file +//reused functions + +function getExpenseTransactions(transactions) { + const expenses = transactions.filter( + (transaction) => transaction.type === 'expense' + ); + return expenses; +} + +function getFirstCharacterToUp(word) { + if (!word) return ''; + return word[0].toUpperCase() + word.slice(1); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..52239c6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,86 @@ +{ + "name": "c55-core-week-4", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "c55-core-week-4", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^4.1.2" + } + }, + "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/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/package.json b/package.json new file mode 100644 index 0000000..3134228 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "c55-core-week-4", + "version": "1.0.0", + "description": "The week 4 assignment for the HackYourFuture Core program can be found at the following link: https://hub.hackyourfuture.nl/core-program-week-4-assignment", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node finance-tracker/app.js" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "^4.1.2" + } +}