#[derive(Debug)]
pub struct MerchantEntry {
pub pattern: &'static str,
pub account: &'static str,
pub category: &'static str,
}
pub static MERCHANT_PATTERNS: &[MerchantEntry] = &[
MerchantEntry {
pattern: r"WHOLE\s*FOODS",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: r"TRADER\s*JOE",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "KROGER",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "SAFEWAY",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "PUBLIX",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "ALDI",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "LIDL",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "COSTCO",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: r"SAM'?S\s*CLUB",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "WEGMANS",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: r"H[\s-]?E[\s-]?B\b",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "MEIJER",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: r"FOOD\s*LION",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "SPROUTS",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "INSTACART",
account: "Expenses:Groceries",
category: "Groceries",
},
MerchantEntry {
pattern: "STARBUCKS",
account: "Expenses:Dining:Coffee",
category: "Dining",
},
MerchantEntry {
pattern: "DUNKIN",
account: "Expenses:Dining:Coffee",
category: "Dining",
},
MerchantEntry {
pattern: r"PEET'?S\s*COFFEE",
account: "Expenses:Dining:Coffee",
category: "Dining",
},
MerchantEntry {
pattern: "MCDONALD",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: r"BURGER\s*KING",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: r"TACO\s*BELL",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: r"CHICK[\s-]?FIL[\s-]?A",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: "SUBWAY",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: "CHIPOTLE",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: "PANERA",
account: "Expenses:Dining:FastFood",
category: "Dining",
},
MerchantEntry {
pattern: "DOORDASH",
account: "Expenses:Dining:Delivery",
category: "Dining",
},
MerchantEntry {
pattern: "GRUBHUB",
account: "Expenses:Dining:Delivery",
category: "Dining",
},
MerchantEntry {
pattern: r"UBER\s*EATS",
account: "Expenses:Dining:Delivery",
category: "Dining",
},
MerchantEntry {
pattern: r"UBER\s*(TRIP|RIDE|BV)",
account: "Expenses:Transport:Rideshare",
category: "Transport",
},
MerchantEntry {
pattern: "LYFT",
account: "Expenses:Transport:Rideshare",
category: "Transport",
},
MerchantEntry {
pattern: r"SHELL\b",
account: "Expenses:Transport:Fuel",
category: "Transport",
},
MerchantEntry {
pattern: "CHEVRON",
account: "Expenses:Transport:Fuel",
category: "Transport",
},
MerchantEntry {
pattern: "EXXON",
account: "Expenses:Transport:Fuel",
category: "Transport",
},
MerchantEntry {
pattern: r"BP\b",
account: "Expenses:Transport:Fuel",
category: "Transport",
},
MerchantEntry {
pattern: "SPEEDWAY",
account: "Expenses:Transport:Fuel",
category: "Transport",
},
MerchantEntry {
pattern: "PARKING",
account: "Expenses:Transport:Parking",
category: "Transport",
},
MerchantEntry {
pattern: r"E[\s-]?Z\s*PASS",
account: "Expenses:Transport:Tolls",
category: "Transport",
},
MerchantEntry {
pattern: "AMAZON|AMZN",
account: "Expenses:Shopping:Amazon",
category: "Shopping",
},
MerchantEntry {
pattern: r"WALMART|WM\s*SUPERCENTER",
account: "Expenses:Shopping",
category: "Shopping",
},
MerchantEntry {
pattern: r"TARGET\b",
account: "Expenses:Shopping",
category: "Shopping",
},
MerchantEntry {
pattern: r"BEST\s*BUY",
account: "Expenses:Shopping:Electronics",
category: "Shopping",
},
MerchantEntry {
pattern: r"APPLE\.COM|APPLE\s*STORE",
account: "Expenses:Shopping:Electronics",
category: "Shopping",
},
MerchantEntry {
pattern: r"HOME\s*DEPOT",
account: "Expenses:Shopping:Home",
category: "Shopping",
},
MerchantEntry {
pattern: r"LOWE'?S",
account: "Expenses:Shopping:Home",
category: "Shopping",
},
MerchantEntry {
pattern: "IKEA",
account: "Expenses:Shopping:Home",
category: "Shopping",
},
MerchantEntry {
pattern: "NETFLIX",
account: "Expenses:Subscriptions:Streaming",
category: "Subscriptions",
},
MerchantEntry {
pattern: "SPOTIFY",
account: "Expenses:Subscriptions:Streaming",
category: "Subscriptions",
},
MerchantEntry {
pattern: "HULU",
account: "Expenses:Subscriptions:Streaming",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"DISNEY\s*\+|DISNEYPLUS",
account: "Expenses:Subscriptions:Streaming",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"HBO\s*MAX|MAX\.COM",
account: "Expenses:Subscriptions:Streaming",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"APPLE\s*(TV|MUSIC|ONE|ICLOUD)",
account: "Expenses:Subscriptions:Apple",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"AMAZON\s*PRIME",
account: "Expenses:Subscriptions:Amazon",
category: "Subscriptions",
},
MerchantEntry {
pattern: "ADOBE",
account: "Expenses:Subscriptions:Software",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"MICROSOFT\s*(365|OFFICE)",
account: "Expenses:Subscriptions:Software",
category: "Subscriptions",
},
MerchantEntry {
pattern: "GITHUB",
account: "Expenses:Subscriptions:Software",
category: "Subscriptions",
},
MerchantEntry {
pattern: "OPENAI|CHATGPT",
account: "Expenses:Subscriptions:Software",
category: "Subscriptions",
},
MerchantEntry {
pattern: r"AT\s*&?\s*T\b",
account: "Expenses:Utilities:Phone",
category: "Utilities",
},
MerchantEntry {
pattern: "VERIZON",
account: "Expenses:Utilities:Phone",
category: "Utilities",
},
MerchantEntry {
pattern: r"T[\s-]?MOBILE",
account: "Expenses:Utilities:Phone",
category: "Utilities",
},
MerchantEntry {
pattern: "COMCAST|XFINITY",
account: "Expenses:Utilities:Internet",
category: "Utilities",
},
MerchantEntry {
pattern: "SPECTRUM",
account: "Expenses:Utilities:Internet",
category: "Utilities",
},
MerchantEntry {
pattern: "CVS",
account: "Expenses:Health:Pharmacy",
category: "Health",
},
MerchantEntry {
pattern: "WALGREENS",
account: "Expenses:Health:Pharmacy",
category: "Health",
},
MerchantEntry {
pattern: r"PLANET\s*FITNESS",
account: "Expenses:Health:Fitness",
category: "Health",
},
MerchantEntry {
pattern: "PELOTON",
account: "Expenses:Health:Fitness",
category: "Health",
},
MerchantEntry {
pattern: "AIRBNB",
account: "Expenses:Travel:Lodging",
category: "Travel",
},
MerchantEntry {
pattern: r"BOOKING\.COM",
account: "Expenses:Travel:Lodging",
category: "Travel",
},
MerchantEntry {
pattern: "MARRIOTT",
account: "Expenses:Travel:Lodging",
category: "Travel",
},
MerchantEntry {
pattern: "HILTON",
account: "Expenses:Travel:Lodging",
category: "Travel",
},
MerchantEntry {
pattern: "EXPEDIA",
account: "Expenses:Travel",
category: "Travel",
},
MerchantEntry {
pattern: "VENMO",
account: "Expenses:Transfers:Venmo",
category: "Financial",
},
MerchantEntry {
pattern: "PAYPAL",
account: "Expenses:Transfers:PayPal",
category: "Financial",
},
MerchantEntry {
pattern: "ZELLE",
account: "Expenses:Transfers:Zelle",
category: "Financial",
},
MerchantEntry {
pattern: "TICKETMASTER",
account: "Expenses:Entertainment",
category: "Entertainment",
},
MerchantEntry {
pattern: r"STEAM\s*(GAMES|PURCHASE)",
account: "Expenses:Entertainment:Games",
category: "Entertainment",
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merchant_patterns_not_empty() {
assert!(!MERCHANT_PATTERNS.is_empty());
assert!(
MERCHANT_PATTERNS.len() > 50,
"Expected at least 50 merchant patterns"
);
}
#[test]
fn all_patterns_compile() {
for entry in MERCHANT_PATTERNS {
let result = regex::RegexBuilder::new(entry.pattern)
.case_insensitive(true)
.build();
assert!(
result.is_ok(),
"Pattern '{}' for {} failed to compile: {:?}",
entry.pattern,
entry.account,
result.err()
);
}
}
#[test]
fn patterns_have_valid_accounts() {
for entry in MERCHANT_PATTERNS {
assert!(
entry.account.starts_with("Expenses:") || entry.account.starts_with("Income:"),
"Pattern '{}' has invalid account '{}' — must start with Expenses: or Income:",
entry.pattern,
entry.account,
);
}
}
#[test]
fn patterns_have_categories() {
for entry in MERCHANT_PATTERNS {
assert!(
!entry.category.is_empty(),
"Pattern '{}' is missing a category",
entry.pattern,
);
}
}
}