MonthYear used for card expiries; bounds 2000..=2099 are intentional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
1.8 KiB
Rust
64 lines
1.8 KiB
Rust
//! Time helpers and the `MonthYear` type used for card expiries.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Current Unix timestamp in seconds.
|
|
pub fn now_unix() -> i64 {
|
|
chrono::Utc::now().timestamp()
|
|
}
|
|
|
|
/// Month + year (1-12 / e.g. 2026). Used for card expiries.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct MonthYear {
|
|
pub month: u8,
|
|
pub year: u16,
|
|
}
|
|
|
|
impl MonthYear {
|
|
pub fn new(month: u8, year: u16) -> Result<Self, &'static str> {
|
|
if !(1..=12).contains(&month) {
|
|
return Err("month must be 1..=12");
|
|
}
|
|
if year < 2000 || year > 2099 {
|
|
return Err("year must be 2000..=2099");
|
|
}
|
|
Ok(Self { month, year })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn now_unix_is_positive_and_recent() {
|
|
let t = now_unix();
|
|
assert!(t > 1_700_000_000); // after late 2023
|
|
assert!(t < 4_000_000_000); // before 2096
|
|
}
|
|
|
|
#[test]
|
|
fn month_year_constructor_rejects_bad_month() {
|
|
assert!(MonthYear::new(0, 2026).is_err());
|
|
assert!(MonthYear::new(13, 2026).is_err());
|
|
assert!(MonthYear::new(1, 2026).is_ok());
|
|
assert!(MonthYear::new(12, 2026).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn month_year_constructor_rejects_bad_year() {
|
|
assert!(MonthYear::new(1, 1999).is_err());
|
|
assert!(MonthYear::new(1, 2100).is_err());
|
|
assert!(MonthYear::new(1, 2000).is_ok());
|
|
assert!(MonthYear::new(1, 2099).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn month_year_round_trips_through_json() {
|
|
let my = MonthYear::new(7, 2030).unwrap();
|
|
let json = serde_json::to_string(&my).unwrap();
|
|
let parsed: MonthYear = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed, my);
|
|
}
|
|
}
|