Adding a Transaction in Rust

Mục tiêu: A function to add transactions with date parsing, amount validation, and error handling in Rust.


To create a function to add a new transaction, we’ll follow these steps:

  1. Define the function signature with appropriate parameters.
  2. Parse the date string into a DateTime object.
  3. Validate the amount to ensure it’s positive.
  4. Create a new Transaction instance.
  5. Add the transaction to the list.
  6. Implement error handling.

Here’s the code:

use chrono::DateTime;
use std::error::Error;

/// Adds a new transaction to the list of transactions.
/// 
/// # Arguments
/// * `date_str` - Date of the transaction in "YYYY-MM-DD" format.
/// * `description` - Description of the transaction.
/// * `amount` - Amount of the transaction. Must be greater than 0.
/// * `transaction_type` - Type of transaction (Income/Expense).
/// * `transactions` - Mutable reference to the vector holding transactions.
/// 
/// # Returns
/// * `Result<(), Box<dyn Error>>` - Returns Ok(()) on success, or an error if date parsing fails or amount is invalid.
/// 
/// # Examples
/// ```
/// let mut transactions = Vec::new();
/// add_transaction("2023-10-01", "Salary", 5000.0, TransactionType::Income, &mut transactions)?;
/// ```
fn add_transaction(
    date_str: &str,
    description: &str,
    amount: f64,
    transaction_type: TransactionType,
    transactions: &mut Vec<Transaction>,
) -> Result<(), Box<dyn Error>> {
    // Parse the date string into a DateTime object
    let date = DateTime::parse_from_str(date_str, "%Y-%m-%d")?;

    // Validate the amount
    if amount <= 0.0 {
        return Err("Amount must be greater than 0".into());
    }

    // Create a new Transaction instance
    let transaction = Transaction {
        date,
        description: description.to_string(),
        amount,
        transaction_type,
    };

    // Add the transaction to the list
    transactions.push(transaction);

    Ok(())
}

Explanation

  • Function Parameters: The function takes the date as a string, description, amount, transaction type, and a mutable reference to the transactions vector.
  • Date Parsing: Uses chrono::parse_from_str to convert the date string into a DateTime object, handling potential parsing errors.
  • Amount Validation: Checks if the amount is positive, returning an error if not.
  • Transaction Creation: Constructs a new Transaction struct and appends it to the vector.
  • Error Handling: Returns a Result type, allowing the caller to handle errors appropriately.

Next Steps

After implementing the add function, you’ll move on to creating a function to remove transactions by description. This function will iterate through the transactions, find the matching description, and remove it if found.

Further Reading

Implement Transaction Removal Function in Rust

Mục tiêu: Implement a function to remove a transaction from a list based on its description in Rust.


Implementing Transaction Removal Function in Rust

Now that we’ve implemented the function to add a new transaction, let’s move on to creating a function to remove a transaction by its description. This is an essential feature for any personal finance tracker, as users will need to correct or remove erroneous entries.

Understanding the Requirements

The function should:

  1. Take a mutable reference to our collection of transactions
  2. Take a string parameter for the description to match
  3. Remove the first transaction that matches the description
  4. Return a boolean indicating whether a transaction was removed

Implementation Strategy

We’ll implement this using a simple loop that checks each transaction’s description. When a match is found, we’ll remove it and return true. If no match is found after checking all transactions, we’ll return false.

The Code

/// Removes a transaction from the list by its description
/// Returns true if a transaction was removed, false otherwise
pub fn remove_transaction(transactions: &mut Vec<Transaction>, description: &str) -> bool {
    for i in 0..transactions.len() {
        if transactions[i].description == description {
            transactions.remove(i);
            return true;
        }
    }
    false
}

Explanation

  • The function remove_transaction takes two parameters: a mutable reference to a vector of Transaction objects and a string slice description.
  • We loop through each transaction using a traditional for loop with indices.
  • For each transaction, we check if its description field matches the provided description.
  • If a match is found, we remove the transaction at the current index using Vec::remove(), which also shifts all subsequent elements to the left.
  • The function returns true immediately after removing the transaction to indicate success.
  • If the loop completes without finding a match, the function returns false.

Important Considerations

  1. Mutability: The function takes a mutable reference (&mut) to the transactions vector because we need to modify it by removing an element.
  2. Ownership: The vector remains owned by the caller, and we’re only borrowing it mutably for the duration of the function.
  3. Matching: The matching is done using a simple string comparison. If you need case-insensitive matching or partial matches, you’ll need to modify the comparison logic accordingly.
  4. Error Handling: This function doesn’t return a Result type because the only “error” condition is when no transaction is found, which is handled by returning false.

Testing the Function

To ensure the function works as expected, you should test it with different scenarios:

  1. Test Case 1: Remove a transaction that exists in the list
  2. Test Case 2: Remove a transaction that doesn’t exist
  3. Test Case 3: Remove a transaction from an empty list
  4. Test Case 4: Remove one of multiple transactions with the same description

Here’s an example of how you might test it:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_remove_transaction() {
        let mut transactions = Vec::new();

        // Create test transactions
        let income = Transaction {
            date: "2023-01-01".to_string(),
            description: "Test Income".to_string(),
            amount: 100.0,
            transaction_type: TransactionType::Income,
        };

        let expense = Transaction {
            date: "2023-01-02".to_string(),
            description: "Test Expense".to_string(),
            amount: 50.0,
            transaction_type: TransactionType::Expense,
        };

        transactions.push(income);
        transactions.push(expense);

        // Test removing an existing transaction
        assert!(remove_transaction(&mut transactions, "Test Income"));

        // Verify the transaction was removed
        assert_eq!(transactions.len(), 1);

        // Test removing a non-existent transaction
        assert!(!remove_transaction(&mut transactions, "Non-existent"));
    }
}

Next Steps

After implementing this function, you should:

  1. Implement the remaining functions in this step (list all transactions)
  2. Move on to implementing file I/O operations
  3. Consider how you’ll handle multiple transactions with the same description
  4. Think about adding more sophisticated filtering options

Further Reading

List Transactions in Personal Finance Tracker

Mục tiêu: Function to display all transactions with details including date, description, amount, and type.


Here’s a detailed solution to create a function to list all transactions in your Personal Finance Tracker:

To implement the transaction listing functionality, we’ll create a function that takes a vector of Transaction instances and displays them in a user-friendly format.

Implementation Code

fn list_transactions(transactions: &Vec<Transaction>) {
    if transactions.is_empty() {
        println!("No transactions found.");
        return;
    }

    println!("List of Transactions:");
    println!("-----------------------");

    for transaction in transactions {
        println!(
            "{} - {} : ${:.2} ({})",
            transaction.date,
            transaction.description,
            transaction.amount,
            transaction.transaction_type
        );
    }

    println!("-----------------------");
}

Explanation

  1. Function Definition: The function list_transactions takes an immutable reference to a vector of Transaction objects.
  2. Empty Check: Before listing transactions, we check if the vector is empty. If it is, we print a message indicating no transactions were found.
  3. Header: Print a header to indicate the start of the transaction list.
  4. Iteration: Loop through each transaction in the vector using a for loop.
  5. Formatted Output: For each transaction, print its details in a formatted string:
  6. Date: Displayed as is
  7. Description: Brief description of the transaction
  8. Amount: Formatted to two decimal places for monetary value
  9. Type: Either “Income” or “Expense” based on the TransactionType enum
  10. Footer: Print a closing line after listing all transactions.

Example Output

List of Transactions:
-----------------------
2023-10-01 - Salary : $1500.00 (Income)
2023-10-02 - Groceries : $45.75 (Expense)
2023-10-03 - Rent : $800.00 (Expense)
-----------------------

Next Steps

  1. Implement remove_transaction Function: Create a function that removes a transaction based on its description.
  2. Error Handling: Add proper error handling for cases where a transaction might not exist.
  3. Testing: Write test cases to verify the functionality of both add_transaction and list_transactions functions.

Further Reading

Implementing Error Handling for Transaction Operations in Rust

Mục tiêu: A task focused on implementing error handling for transaction operations using Rust’s Result type and custom error enums.


Implementing Error Handling for Transaction Operations in Rust

Error handling is a crucial aspect of building robust applications. In Rust, we can leverage the Result type and Option type to handle errors in a clean and expressive way. Let’s implement error handling for our transaction operations.

Step-by-Step Implementation

  1. Define Custom Error Types

We’ll start by defining a custom error enum that represents the possible errors our application can encounter:

#[derive(Debug)]
enum TransactionError {
TransactionNotFound,
InvalidAmount,
IoError(std::io::Error),
}

impl std::error::Error for TransactionError {}

impl From for TransactionError {
fn from(e: std::io::Error) -> Self {
TransactionError::IoError(e)
}
}
  • TransactionNotFound: Occurs when trying to remove a transaction that doesn’t exist
  • InvalidAmount: Occurs when a negative amount is provided
  • IoError: Wraps I/O errors that might occur during file operations
  • Modify Transaction Functions to Return Results

Let’s update our transaction functions to return Result types:

fn add\_transaction(transactions: &mut Vec, transaction: Transaction) -> Result<(), TransactionError> {
if transaction.amount < 0.0 {
return Err(TransactionError::InvalidAmount);
}
transactions.push(transaction);
Ok(())
}

fn remove\_transaction(transactions: &mut Vec, description: &str) -> Result<(), TransactionError> {
let index = transactions.iter().position(|t| t.description == description);
match index {
Some(i) => {
transactions.remove(i);
Ok(())
}
None => Err(TransactionError::TransactionNotFound),
}
}

fn list\_transactions(transactions: &Vec Result, TransactionError> {
Ok(transactions.clone())
}
  • add_transaction: Now returns a Result indicating success or an InvalidAmount error
  • remove_transaction: Returns a Result indicating success or TransactionNotFound
  • list_transactions: Returns a Result with the list of transactions
  • Handling Errors in Caller Code

When calling these functions, we need to handle potential errors. Here’s how you might do it:

fn main() {
let mut transactions = Vec::new();

// Adding a transaction let transaction = Transaction { date: “2023-10-01”.to_string(), description: “Rent”.to_string(), amount: 1000.0, transaction_type: TransactionType::Expense, };

match add_transaction(&mut transactions, transaction) { Ok(_) => println!(“Transaction added successfully”), Err(TransactionError::InvalidAmount) => println!(“Error: Transaction amount cannot be negative”), Err(e) => println!(“An unexpected error occurred: {:?}”, e), }

// Removing a transaction match remove_transaction(&mut transactions, “Rent”) { Ok(_) => println!(“Transaction removed successfully”), Err(TransactionError::TransactionNotFound) => println!(“Error: Transaction not found”), Err(e) => println!(“An unexpected error occurred: {:?}”, e), }

// Listing transactions match list_transactions(&transactions) { Ok(ts) => println!(“Current transactions: {:?}”, ts), Err(e) => println!(“Error fetching transactions: {:?}”, e), }


}
  1. Best Practices for Error Handling
  2. Return Results: Instead of panicking, functions should return Result types to allow callers to handle errors
  3. Use Custom Errors: Custom error types make error handling more explicit and user-friendly
  4. Handle Errors Locally: Handle errors as close to their point of occurrence as possible
  5. Provide Context: When logging errors, provide sufficient context for debugging

Next Steps

Now that we’ve implemented error handling for transaction operations, you can proceed to:

  1. Implement CSV file operations with proper error handling
  2. Add validation for user inputs
  3. Implement more sophisticated error handling for file I/O operations

Further Reading

By following these practices, your application will be more robust and user-friendly, providing clear feedback when something goes wrong.