DEV Community

Putra Prima A
Putra Prima A

Posted on

Dart Object Oriented For Beginner : Expense Manager Case Study Part 6

Lesson 6: Managing Multiple Expenses

Duration: 40 minutes

App Feature: πŸ“Š Creating an Expense Manager

What You'll Build: A class to manage all your expenses together

Prerequisites: Complete Lessons 1-5


What We'll Learn Today

By the end of this lesson, you'll be able to:

  • βœ… Create a manager class to handle collections of objects
  • βœ… Work with Lists of expenses
  • βœ… Add, remove, and update expenses
  • βœ… Filter expenses by various criteria
  • βœ… Calculate totals and statistics
  • βœ… Generate reports from multiple expenses
  • βœ… Sort expenses in different ways

Part 1: Introduction to Collections

Before we build the ExpenseManager, let's understand Lists:

void main() {
  var manager = ExpenseManager();

  // Add expenses
  manager.addExpense(Expense.quick('Groceries', 127.50, 'Food'));
  manager.addExpense(Expense.quick('Coffee', 4.50, 'Food'));
  manager.addExpense(Expense.quick('Dinner', 45.00, 'Food'));
  manager.addExpense(Expense.quick('Gas', 60.00, 'Transport'));

  // Set budgets
  manager.setBudget('Food', 150.0);
  manager.setBudget('Transport', 100.0);

  // Check budget status
  manager.printBudgetReport();
}
Enter fullscreen mode Exit fullscreen mode

Exercise 3: Advanced Analytics (Hard)

Add these advanced methods:

  • getTopExpenses(int n) - return top N most expensive expenses
  • getCategoryTrend(String category, int days) - return daily average for category over last N days
  • predictMonthlyTotal() - predict month total based on current spending rate

Solution:

class ExpenseManager {
  List<Expense> _expenses = [];

  List<Expense> getTopExpenses(int n) {
    List<Expense> sorted = sortByAmountDesc();
    if (n >= sorted.length) return sorted;
    return sorted.sublist(0, n);
  }

  double getCategoryTrend(String category, int days) {
    DateTime cutoff = DateTime.now().subtract(Duration(days: days));
    var recentExpenses = _expenses.where((e) {
      return e.category == category && e.date.isAfter(cutoff);
    }).toList();

    if (recentExpenses.isEmpty) return 0;

    double total = recentExpenses.fold(0.0, (sum, e) => sum + e.amount);
    return total / days;
  }

  double predictMonthlyTotal() {
    if (_expenses.isEmpty) return 0;

    DateTime now = DateTime.now();
    var thisMonthExpenses = _expenses.where((e) {
      return e.date.year == now.year && e.date.month == now.month;
    }).toList();

    if (thisMonthExpenses.isEmpty) return 0;

    double totalSoFar = thisMonthExpenses.fold(0.0, (sum, e) => sum + e.amount);
    int daysPassed = now.day;
    int daysInMonth = DateTime(now.year, now.month + 1, 0).day;

    double dailyAverage = totalSoFar / daysPassed;
    return dailyAverage * daysInMonth;
  }

  Map<String, double> getCategoryTrends(int days) {
    Map<String, double> trends = {};
    var categories = getAllCategories();

    for (var category in categories) {
      trends[category] = getCategoryTrend(category, days);
    }

    return trends;
  }

  void printTrendReport(int days) {
    print('\nπŸ“ˆ SPENDING TRENDS (Last $days days)\n');
    var trends = getCategoryTrends(days);

    trends.forEach((category, dailyAvg) {
      double monthlyProjection = dailyAvg * 30;
      print('$category:');
      print('  Daily average: \${dailyAvg.toStringAsFixed(2)}');
      print('  Monthly projection: \${monthlyProjection.toStringAsFixed(2)}');
      print('');
    });

    print('πŸ’‘ Predicted month total: \${predictMonthlyTotal().toStringAsFixed(2)}');
  }
}

void main() {
  var manager = ExpenseManager();

  // Add expenses spread over 15 days
  for (int i = 0; i < 15; i++) {
    var date = DateTime.now().subtract(Duration(days: i));
    manager.addExpense(Expense(
      description: 'Daily expense $i',
      amount: 10.0 + (i * 2),
      category: i % 2 == 0 ? 'Food' : 'Transport',
      date: date,
    ));
  }

  print('πŸ† TOP 5 EXPENSES:');
  var top5 = manager.getTopExpenses(5);
  for (int i = 0; i < top5.length; i++) {
    print('${i + 1}. ${top5[i].description} - \${top5[i].amount}');
  }

  manager.printTrendReport(15);
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes & How to Fix Them

Mistake 1: Modifying Original List

// ❌ Wrong - returns reference to internal list
List<Expense> getAllExpenses() {
  return _expenses;  // Can be modified by caller!
}

// βœ… Correct - return a copy
List<Expense> getAllExpenses() {
  return List.from(_expenses);
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Not Checking for Empty Lists

// ❌ Wrong - will crash if empty
Expense getLargestExpense() {
  return _expenses[0];  // Error if empty!
}

// βœ… Correct - check first
Expense? getLargestExpense() {
  if (_expenses.isEmpty) return null;
  return _expenses.reduce((a, b) => a.amount > b.amount ? a : b);
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Inefficient Filtering

// ❌ Wrong - creates multiple loops
List<Expense> getThisMonthFood() {
  var thisMonth = getThisMonth();
  List<Expense> food = [];
  for (var e in thisMonth) {
    if (e.category == 'Food') food.add(e);
  }
  return food;
}

// βœ… Correct - one pass
List<Expense> getThisMonthFood() {
  return _expenses.where((e) => 
    e.isThisMonth() && e.category == 'Food'
  ).toList();
}
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Not Validating Indices

// ❌ Wrong - no validation
void removeAt(int index) {
  _expenses.removeAt(index);  // Crash if invalid!
}

// βœ… Correct - check bounds
bool removeAt(int index) {
  if (index < 0 || index >= _expenses.length) {
    return false;
  }
  _expenses.removeAt(index);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts Review

βœ… Manager classes handle collections of objects

βœ… List operations add, remove, filter, sort data

βœ… Filter methods return subsets based on criteria

βœ… Statistics methods calculate totals, averages, etc.

βœ… Sort methods order data in different ways

βœ… Report methods display information clearly

βœ… Always return copies of internal lists to protect data

βœ… Check for empty lists before operations


Self-Check Questions

1. Why should getAllExpenses() return a copy of the list instead of the original?

Answer:
Returning a copy protects the internal list from being modified by external code. If you return the original list, callers can add/remove items directly, bypassing your manager's control and validation.

2. What's the difference between filter methods and sort methods?

Answer:

  • Filter methods: Return a subset of expenses that meet certain criteria (e.g., only Food expenses)
  • Sort methods: Return all expenses but in a different order (e.g., sorted by amount)

3. When should you use where() vs a for loop for filtering?

Answer:
Use where() when possible - it's more concise and functional. Use a for loop when you need more complex logic or need to modify items during iteration.


What's Next?

In Lesson 7: Inheritance - Different Types of Expenses, we'll learn:

  • Creating specialized expense types
  • RecurringExpense class (monthly bills, subscriptions)
  • OneTimeExpense class (special occasions)
  • Using inheritance to reuse code
  • The "IS-A" relationship

Example preview:

class RecurringExpense extends Expense {
  String frequency;  // 'weekly', 'monthly', 'yearly'

  double yearlyTotal() {
    if (frequency == 'monthly') return amount * 12;
    if (frequency == 'weekly') return amount * 52;
    return amount;
  }
}
Enter fullscreen mode Exit fullscreen mode

See you in Lesson 7! πŸš€


Additional Resources

  • Practice adding more filter methods for different scenarios
  • Try implementing a search feature that searches across all fields
  • Experiment with different sorting combinations
  • Think about what reports would be useful for your expense app

Remember: The ExpenseManager is the brain of your app - it organizes, analyzes, and presents your data. Make it smart and efficient! πŸ’‘


Part 1: Introduction to Collections

Before we build the ExpenseManager, let's understand Lists:

void main() {
  // Creating a list of expenses
  List<Expense> expenses = [];

  // Adding expenses
  expenses.add(Expense.quick('Coffee', 4.50, 'Food'));
  expenses.add(Expense.quick('Lunch', 12.75, 'Food'));
  expenses.add(Expense.quick('Gas', 45.00, 'Transport'));

  // Accessing expenses
  print('First expense: ${expenses[0].description}');
  print('Total count: ${expenses.length}');

  // Looping through expenses
  print('\nAll expenses:');
  for (var expense in expenses) {
    print(expense.getSummary());
  }

  // Removing an expense
  expenses.removeAt(1);  // Remove lunch
  print('\nAfter removing lunch: ${expenses.length} expenses');
}
Enter fullscreen mode Exit fullscreen mode

Key List Operations:

  • add(item) - Add to end
  • insert(index, item) - Add at specific position
  • removeAt(index) - Remove by position
  • remove(item) - Remove by value
  • clear() - Remove all
  • length - Get count
  • [] - Access by index

Part 2: Basic ExpenseManager Class

Let's create our first version:

class ExpenseManager {
  // Private list to store all expenses
  List<Expense> _expenses = [];

  // Add an expense
  void addExpense(Expense expense) {
    _expenses.add(expense);
    print('βœ… Added: ${expense.description}');
  }

  // Get all expenses (return a copy to protect internal list)
  List<Expense> getAllExpenses() {
    return List.from(_expenses);
  }

  // Get total number of expenses
  int getCount() {
    return _expenses.length;
  }

  // Calculate total spending
  double getTotalSpending() {
    double total = 0;
    for (var expense in _expenses) {
      total += expense.amount;
    }
    return total;
  }

  // Print a simple summary
  void printSummary() {
    print('\nπŸ’° EXPENSE SUMMARY');
    print('Total expenses: ${getCount()}');
    print('Total spent: \$${getTotalSpending().toStringAsFixed(2)}');
  }
}

void main() {
  var manager = ExpenseManager();

  manager.addExpense(Expense.quick('Coffee', 4.50, 'Food'));
  manager.addExpense(Expense.quick('Uber', 12.00, 'Transport'));
  manager.addExpense(Expense.quick('Lunch', 15.75, 'Food'));

  manager.printSummary();

  print('\nAll expenses:');
  for (var expense in manager.getAllExpenses()) {
    print(expense.getSummary());
  }
}
Enter fullscreen mode Exit fullscreen mode

Output:

βœ… Added: Coffee
βœ… Added: Uber
βœ… Added: Lunch

πŸ’° EXPENSE SUMMARY
Total expenses: 3
Total spent: $32.25

All expenses:
🟒 Coffee: $4.50 [Food]
🟒 Uber: $12.00 [Transport]
🟒 Lunch: $15.75 [Food]
Enter fullscreen mode Exit fullscreen mode

Part 3: Filtering Expenses

Add methods to filter expenses by different criteria:

class ExpenseManager {
  List<Expense> _expenses = [];

  void addExpense(Expense expense) {
    _expenses.add(expense);
  }

  List<Expense> getAllExpenses() => List.from(_expenses);

  // Filter by category
  List<Expense> getByCategory(String category) {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (expense.category == category) {
        filtered.add(expense);
      }
    }
    return filtered;
  }

  // Filter by amount range
  List<Expense> getByAmountRange(double min, double max) {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (expense.amount >= min && expense.amount <= max) {
        filtered.add(expense);
      }
    }
    return filtered;
  }

  // Get major expenses only
  List<Expense> getMajorExpenses() {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (expense.isMajorExpense()) {
        filtered.add(expense);
      }
    }
    return filtered;
  }

  // Get this month's expenses
  List<Expense> getThisMonth() {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (expense.isThisMonth()) {
        filtered.add(expense);
      }
    }
    return filtered;
  }

  // Get paid/unpaid expenses
  List<Expense> getPaidExpenses() {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (expense.isPaid) {
        filtered.add(expense);
      }
    }
    return filtered;
  }

  List<Expense> getUnpaidExpenses() {
    List<Expense> filtered = [];
    for (var expense in _expenses) {
      if (!expense.isPaid) {
        filtered.add(expense);
      }
    }
    return filtered;
  }
}

void main() {
  var manager = ExpenseManager();

  manager.addExpense(Expense(description: 'Coffee', amount: 4.50, category: 'Food'));
  manager.addExpense(Expense(description: 'Rent', amount: 1200.0, category: 'Bills', isPaid: true));
  manager.addExpense(Expense(description: 'Laptop', amount: 899.99, category: 'Electronics'));
  manager.addExpense(Expense(description: 'Lunch', amount: 15.75, category: 'Food'));

  print('FOOD EXPENSES:');
  for (var expense in manager.getByCategory('Food')) {
    print(expense.getSummary());
  }

  print('\nMAJOR EXPENSES (>$100):');
  for (var expense in manager.getMajorExpenses()) {
    print(expense.getSummary());
  }

  print('\nUNPAID EXPENSES:');
  for (var expense in manager.getUnpaidExpenses()) {
    print(expense.getSummary());
  }
}
Enter fullscreen mode Exit fullscreen mode

Part 4: Advanced Statistics

Add methods to calculate useful statistics:

class ExpenseManager {
  List<Expense> _expenses = [];

  void addExpense(Expense expense) => _expenses.add(expense);

  // Total spending
  double getTotalSpending() {
    double total = 0;
    for (var expense in _expenses) {
      total += expense.amount;
    }
    return total;
  }

  // Total by category
  double getTotalByCategory(String category) {
    double total = 0;
    for (var expense in _expenses) {
      if (expense.category == category) {
        total += expense.amount;
      }
    }
    return total;
  }

  // Average expense amount
  double getAverageExpense() {
    if (_expenses.isEmpty) return 0;
    return getTotalSpending() / _expenses.length;
  }

  // Largest expense
  Expense? getLargestExpense() {
    if (_expenses.isEmpty) return null;

    Expense largest = _expenses[0];
    for (var expense in _expenses) {
      if (expense.amount > largest.amount) {
        largest = expense;
      }
    }
    return largest;
  }

  // Smallest expense
  Expense? getSmallestExpense() {
    if (_expenses.isEmpty) return null;

    Expense smallest = _expenses[0];
    for (var expense in _expenses) {
      if (expense.amount < smallest.amount) {
        smallest = expense;
      }
    }
    return smallest;
  }

  // Count by category
  int countByCategory(String category) {
    int count = 0;
    for (var expense in _expenses) {
      if (expense.category == category) {
        count++;
      }
    }
    return count;
  }

  // Get all unique categories
  List<String> getAllCategories() {
    List<String> categories = [];
    for (var expense in _expenses) {
      if (!categories.contains(expense.category)) {
        categories.add(expense.category);
      }
    }
    return categories;
  }

  // Total unpaid amount
  double getTotalUnpaid() {
    double total = 0;
    for (var expense in _expenses) {
      if (!expense.isPaid) {
        total += expense.amount;
      }
    }
    return total;
  }

  // Get category breakdown (map of category -> total)
  Map<String, double> getCategoryBreakdown() {
    Map<String, double> breakdown = {};
    for (var expense in _expenses) {
      if (breakdown.containsKey(expense.category)) {
        breakdown[expense.category] = breakdown[expense.category]! + expense.amount;
      } else {
        breakdown[expense.category] = expense.amount;
      }
    }
    return breakdown;
  }
}

void main() {
  var manager = ExpenseManager();

  manager.addExpense(Expense(description: 'Coffee', amount: 4.50, category: 'Food'));
  manager.addExpense(Expense(description: 'Rent', amount: 1200.0, category: 'Bills', isPaid: true));
  manager.addExpense(Expense(description: 'Laptop', amount: 899.99, category: 'Electronics'));
  manager.addExpense(Expense(description: 'Lunch', amount: 15.75, category: 'Food'));
  manager.addExpense(Expense(description: 'Gas', amount: 45.00, category: 'Transport'));

  print('πŸ“Š STATISTICS:\n');
  print('Total spending: \$${manager.getTotalSpending().toStringAsFixed(2)}');
  print('Average expense: \$${manager.getAverageExpense().toStringAsFixed(2)}');
  print('Total unpaid: \$${manager.getTotalUnpaid().toStringAsFixed(2)}');

  var largest = manager.getLargestExpense();
  if (largest != null) {
    print('Largest: ${largest.description} - \$${largest.amount}');
  }

  var smallest = manager.getSmallestExpense();
  if (smallest != null) {
    print('Smallest: ${smallest.description} - \$${smallest.amount}');
  }

  print('\nπŸ“ CATEGORY BREAKDOWN:');
  var breakdown = manager.getCategoryBreakdown();
  breakdown.forEach((category, total) {
    print('$category: \$${total.toStringAsFixed(2)}');
  });
}
Enter fullscreen mode Exit fullscreen mode

Part 5: Sorting Expenses

Add methods to sort expenses in different ways:

class ExpenseManager {
  List<Expense> _expenses = [];

  void addExpense(Expense expense) => _expenses.add(expense);
  List<Expense> getAllExpenses() => List.from(_expenses);

  // Sort by amount (ascending)
  List<Expense> sortByAmountAsc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.amount.compareTo(b.amount));
    return sorted;
  }

  // Sort by amount (descending)
  List<Expense> sortByAmountDesc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => b.amount.compareTo(a.amount));
    return sorted;
  }

  // Sort by date (newest first)
  List<Expense> sortByDateDesc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => b.date.compareTo(a.date));
    return sorted;
  }

  // Sort by date (oldest first)
  List<Expense> sortByDateAsc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.date.compareTo(b.date));
    return sorted;
  }

  // Sort by category
  List<Expense> sortByCategory() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.category.compareTo(b.category));
    return sorted;
  }

  // Sort by description
  List<Expense> sortByDescription() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.description.compareTo(b.description));
    return sorted;
  }
}

void main() {
  var manager = ExpenseManager();

  manager.addExpense(Expense(description: 'Laptop', amount: 899.99, category: 'Electronics', date: DateTime(2025, 10, 5)));
  manager.addExpense(Expense(description: 'Coffee', amount: 4.50, category: 'Food', date: DateTime(2025, 10, 9)));
  manager.addExpense(Expense(description: 'Rent', amount: 1200.0, category: 'Bills', date: DateTime(2025, 10, 1)));
  manager.addExpense(Expense(description: 'Lunch', amount: 15.75, category: 'Food', date: DateTime(2025, 10, 8)));

  print('SORTED BY AMOUNT (highest first):');
  for (var expense in manager.sortByAmountDesc()) {
    print('${expense.description}: \$${expense.amount}');
  }

  print('\nSORTED BY DATE (newest first):');
  for (var expense in manager.sortByDateDesc()) {
    print('${expense.description}: ${expense.getFormattedDate()}');
  }

  print('\nSORTED BY CATEGORY:');
  for (var expense in manager.sortByCategory()) {
    print('${expense.category}: ${expense.description}');
  }
}
Enter fullscreen mode Exit fullscreen mode

Part 6: Removing and Updating Expenses

Add methods to modify the expense list:

class ExpenseManager {
  List<Expense> _expenses = [];

  void addExpense(Expense expense) {
    _expenses.add(expense);
    print('βœ… Added: ${expense.description}');
  }

  // Remove expense by index
  bool removeExpenseAt(int index) {
    if (index < 0 || index >= _expenses.length) {
      print('❌ Invalid index');
      return false;
    }
    var removed = _expenses.removeAt(index);
    print('πŸ—‘οΈ Removed: ${removed.description}');
    return true;
  }

  // Remove expense by description
  bool removeExpenseByDescription(String description) {
    for (int i = 0; i < _expenses.length; i++) {
      if (_expenses[i].description == description) {
        var removed = _expenses.removeAt(i);
        print('πŸ—‘οΈ Removed: ${removed.description}');
        return true;
      }
    }
    print('❌ Expense not found: $description');
    return false;
  }

  // Remove all expenses in a category
  int removeByCategory(String category) {
    int count = 0;
    _expenses.removeWhere((expense) {
      if (expense.category == category) {
        count++;
        return true;
      }
      return false;
    });
    print('πŸ—‘οΈ Removed $count expenses from category: $category');
    return count;
  }

  // Clear all expenses
  void clearAll() {
    int count = _expenses.length;
    _expenses.clear();
    print('πŸ—‘οΈ Cleared all $count expenses');
  }

  // Update expense at index
  bool updateExpense(int index, Expense newExpense) {
    if (index < 0 || index >= _expenses.length) {
      print('❌ Invalid index');
      return false;
    }
    _expenses[index] = newExpense;
    print('✏️ Updated expense at index $index');
    return true;
  }

  // Get expense by index
  Expense? getExpenseAt(int index) {
    if (index < 0 || index >= _expenses.length) {
      return null;
    }
    return _expenses[index];
  }

  // Find index of expense by description
  int findIndexByDescription(String description) {
    for (int i = 0; i < _expenses.length; i++) {
      if (_expenses[i].description == description) {
        return i;
      }
    }
    return -1;
  }
}

void main() {
  var manager = ExpenseManager();

  manager.addExpense(Expense.quick('Coffee', 4.50, 'Food'));
  manager.addExpense(Expense.quick('Lunch', 15.75, 'Food'));
  manager.addExpense(Expense.quick('Gas', 45.00, 'Transport'));

  print('\n--- Removing ---');
  manager.removeExpenseByDescription('Coffee');

  print('\n--- Updating ---');
  int index = manager.findIndexByDescription('Lunch');
  if (index != -1) {
    manager.updateExpense(index, Expense.quick('Dinner', 25.00, 'Food'));
  }

  print('\n--- Final list ---');
  for (var expense in manager.getAllExpenses()) {
    print(expense.getSummary());
  }
}
Enter fullscreen mode Exit fullscreen mode

Part 7: Complete ExpenseManager Class

Here's the full-featured ExpenseManager:

class ExpenseManager {
  List<Expense> _expenses = [];

  // === ADD METHODS ===
  void addExpense(Expense expense) {
    _expenses.add(expense);
  }

  void addMultipleExpenses(List<Expense> expenses) {
    _expenses.addAll(expenses);
    print('βœ… Added ${expenses.length} expenses');
  }

  // === GET METHODS ===
  List<Expense> getAllExpenses() => List.from(_expenses);

  int getCount() => _expenses.length;

  bool isEmpty() => _expenses.isEmpty;

  Expense? getExpenseAt(int index) {
    if (index < 0 || index >= _expenses.length) return null;
    return _expenses[index];
  }

  // === FILTER METHODS ===
  List<Expense> getByCategory(String category) {
    return _expenses.where((e) => e.category == category).toList();
  }

  List<Expense> getByAmountRange(double min, double max) {
    return _expenses.where((e) => e.amount >= min && e.amount <= max).toList();
  }

  List<Expense> getMajorExpenses() {
    return _expenses.where((e) => e.isMajorExpense()).toList();
  }

  List<Expense> getThisMonth() {
    return _expenses.where((e) => e.isThisMonth()).toList();
  }

  List<Expense> getPaidExpenses() {
    return _expenses.where((e) => e.isPaid).toList();
  }

  List<Expense> getUnpaidExpenses() {
    return _expenses.where((e) => !e.isPaid).toList();
  }

  // === STATISTICS METHODS ===
  double getTotalSpending() {
    return _expenses.fold(0.0, (sum, e) => sum + e.amount);
  }

  double getTotalByCategory(String category) {
    return _expenses
        .where((e) => e.category == category)
        .fold(0.0, (sum, e) => sum + e.amount);
  }

  double getAverageExpense() {
    if (_expenses.isEmpty) return 0;
    return getTotalSpending() / _expenses.length;
  }

  double getTotalUnpaid() {
    return _expenses
        .where((e) => !e.isPaid)
        .fold(0.0, (sum, e) => sum + e.amount);
  }

  Expense? getLargestExpense() {
    if (_expenses.isEmpty) return null;
    return _expenses.reduce((a, b) => a.amount > b.amount ? a : b);
  }

  Expense? getSmallestExpense() {
    if (_expenses.isEmpty) return null;
    return _expenses.reduce((a, b) => a.amount < b.amount ? a : b);
  }

  int countByCategory(String category) {
    return _expenses.where((e) => e.category == category).length;
  }

  List<String> getAllCategories() {
    return _expenses.map((e) => e.category).toSet().toList();
  }

  Map<String, double> getCategoryBreakdown() {
    Map<String, double> breakdown = {};
    for (var expense in _expenses) {
      breakdown[expense.category] = 
          (breakdown[expense.category] ?? 0) + expense.amount;
    }
    return breakdown;
  }

  Map<String, int> getCategoryCounts() {
    Map<String, int> counts = {};
    for (var expense in _expenses) {
      counts[expense.category] = (counts[expense.category] ?? 0) + 1;
    }
    return counts;
  }

  // === SORT METHODS ===
  List<Expense> sortByAmountDesc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => b.amount.compareTo(a.amount));
    return sorted;
  }

  List<Expense> sortByAmountAsc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.amount.compareTo(b.amount));
    return sorted;
  }

  List<Expense> sortByDateDesc() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => b.date.compareTo(a.date));
    return sorted;
  }

  List<Expense> sortByCategory() {
    List<Expense> sorted = List.from(_expenses);
    sorted.sort((a, b) => a.category.compareTo(b.category));
    return sorted;
  }

  // === REMOVE METHODS ===
  bool removeExpenseAt(int index) {
    if (index < 0 || index >= _expenses.length) return false;
    _expenses.removeAt(index);
    return true;
  }

  bool removeExpenseByDescription(String description) {
    int initialLength = _expenses.length;
    _expenses.removeWhere((e) => e.description == description);
    return _expenses.length < initialLength;
  }

  int removeByCategory(String category) {
    int initialLength = _expenses.length;
    _expenses.removeWhere((e) => e.category == category);
    return initialLength - _expenses.length;
  }

  void clearAll() {
    _expenses.clear();
  }

  // === SEARCH METHODS ===
  List<Expense> searchByDescription(String query) {
    String lowerQuery = query.toLowerCase();
    return _expenses
        .where((e) => e.description.toLowerCase().contains(lowerQuery))
        .toList();
  }

  int findIndexByDescription(String description) {
    return _expenses.indexWhere((e) => e.description == description);
  }

  // === REPORT METHODS ===
  void printSummary() {
    print('\n═══════════════════════════════════');
    print('πŸ’° EXPENSE SUMMARY');
    print('═══════════════════════════════════');
    print('Total expenses: ${getCount()}');
    print('Total spent: \$${getTotalSpending().toStringAsFixed(2)}');
    print('Average expense: \$${getAverageExpense().toStringAsFixed(2)}');
    print('Total unpaid: \$${getTotalUnpaid().toStringAsFixed(2)}');

    var largest = getLargestExpense();
    if (largest != null) {
      print('Largest: ${largest.description} (\$${largest.amount})');
    }

    print('═══════════════════════════════════\n');
  }

  void printCategoryReport() {
    print('\nπŸ“Š CATEGORY BREAKDOWN\n');
    var breakdown = getCategoryBreakdown();
    var counts = getCategoryCounts();
    double total = getTotalSpending();

    breakdown.forEach((category, amount) {
      double percentage = (amount / total) * 100;
      int count = counts[category] ?? 0;
      print('$category:');
      print('  Amount: \$${amount.toStringAsFixed(2)} (${percentage.toStringAsFixed(1)}%)');
      print('  Count: $count expenses');
      print('');
    });
  }

  void printAllExpenses() {
    print('\nπŸ“‹ ALL EXPENSES\n');
    if (_expenses.isEmpty) {
      print('No expenses to display');
      return;
    }

    for (int i = 0; i < _expenses.length; i++) {
      print('${i + 1}. ${_expenses[i].getFullDisplay()}');
    }
    print('');
  }
}

void main() {
  var manager = ExpenseManager();

  // Add expenses
  manager.addExpense(Expense(description: 'Coffee', amount: 4.50, category: 'Food'));
  manager.addExpense(Expense(description: 'Rent', amount: 1200.0, category: 'Bills', isPaid: true));
  manager.addExpense(Expense(description: 'Laptop', amount: 899.99, category: 'Electronics'));
  manager.addExpense(Expense(description: 'Lunch', amount: 15.75, category: 'Food'));
  manager.addExpense(Expense(description: 'Gas', amount: 45.00, category: 'Transport'));
  manager.addExpense(Expense(description: 'Groceries', amount: 127.50, category: 'Food', isPaid: true));

  // Print reports
  manager.printSummary();
  manager.printCategoryReport();
  manager.printAllExpenses();

  // Filter and display
  print('πŸ” FOOD EXPENSES:');
  for (var expense in manager.getByCategory('Food')) {
    print('  ${expense.getSummary()}');
  }

  print('\nπŸ”΄ MAJOR EXPENSES:');
  for (var expense in manager.getMajorExpenses()) {
    print('  ${expense.getSummary()}');
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Practice Exercises

Exercise 1: Monthly Report (Easy)

Add a method to ExpenseManager:

  • getMonthlyReport(int year, int month) - returns all expenses for that month
  • getMonthlyTotal(int year, int month) - returns total for that month

Solution:

List<Expense> getMonthlyReport(int year, int month) {
  return _expenses.where((e) {
    return e.date.year == year && e.date.month == month;
  }).toList();
}

double getMonthlyTotal(int year, int month) {
  return getMonthlyReport(year, month)
      .fold(0.0, (sum, e) => sum + e.amount);
}

void main() {
  var manager = ExpenseManager();
  // Add expenses...

  var octoberExpenses = manager.getMonthlyReport(2025, 10);
  print('October expenses: ${octoberExpenses.length}');
  print('October total: \$${manager.getMonthlyTotal(2025, 10).toStringAsFixed(2)}');
}
Enter fullscreen mode Exit fullscreen mode

Exercise 2: Budget Tracking (Medium)

Add these methods to ExpenseManager:

  • setBudget(String category, double amount) - set budget for a category
  • isOverBudget(String category) - check if category is over budget
  • getBudgetStatus(String category) - return how much under/over

Solution:


dart
class ExpenseManager {
  List<Expense> _expenses = [];
  Map<String, double> _budgets = {};

  void setBudget(String category, double amount) {
    _budgets[category] = amount;
    print('πŸ’° Set budget for $category: \$${amount.toStringAsFixed(2)}');
  }

  bool isOverBudget(String category) {
    if (!_budgets.containsKey(category)) return false;
    double spent = getTotalByCategory(category);
    return spent > _budgets[category]!;
  }

  String getBudgetStatus(String category) {
    if (!_budgets.containsKey(category)) {
      return 'No budget set for $category';
    }

    double budget = _budgets[category]!;
    double spent = getTotalByCategory(category);
    double remaining = budget - spent;

    if (remaining >= 0) {
      return '\$${remaining.toStringAsFixed(2)} remaining (${((spent/budget)*100).toStringAsFixed(1)}% used)';
    } else {
      return '\$${remaining.abs().toStringAsFixed(2)} over budget!';
    }
  }

  void printBudgetReport() {
    print('\nπŸ’° BUDGET REPORT\n');
    _budgets.forEach((category, budget) {
      double spent = getTotalByCategory(category);
      String status = isOverBudget(category) ? '❌' : 'βœ…';
      print('$status $category:');
      print('   Budget: \$${budget.toStringAsFixed(2)}');
      print('   Spent: \$${spent.toStringAsFixed(2)}');
      print('   ${getBudgetStatus(category)}');
      print('');
    });
  }
}

void main() {
Enter fullscreen mode Exit fullscreen mode

Top comments (0)