//! Takvim hesapları: tarih okuma/yazma, dönem haftaları, Türkçe ay adları. //! //! Tarihler veritabanında `"YYYY-MM-DD"` metni olarak tutulur (DATA_MODEL.md §1.3). //! Dönem bitişi **hariçtir**: `2026-12-01 – 2027-01-01` = 1–31 Aralık. use time::{Date, Duration, Month}; const MONTHS: [&str; 12] = [ "Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık", ]; const MONTHS_SHORT: [&str; 12] = [ "Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara", ]; /// `"2026-12-09"` → `Date`. Biçim ya da tarih geçersizse `None` (ör. `2026-02-30`). pub fn parse_date(s: &str) -> Option { let b = s.as_bytes(); if b.len() != 10 || b[4] != b'-' || b[7] != b'-' { return None; } let year: i32 = s[0..4].parse().ok()?; let month: u8 = s[5..7].parse().ok()?; let day: u8 = s[8..10].parse().ok()?; Date::from_calendar_date(year, Month::try_from(month).ok()?, day).ok() } /// `Date` → `"2026-12-09"` /// Türkiye'de bugünün tarihi (UTC+3, yaz saati yok). Sunucu UTC'de çalışsa da gün doğru olsun. pub fn today_tr() -> Date { time::OffsetDateTime::now_utc() .to_offset(time::UtcOffset::from_hms(3, 0, 0).expect("geçerli ofset")) .date() } pub fn format_date(d: Date) -> String { format!("{:04}-{:02}-{:02}", d.year(), u8::from(d.month()), d.day()) } /// Bitişi hariç aralığın gün sayısı. pub fn days_between(start: Date, end_exclusive: Date) -> i64 { (end_exclusive - start).whole_days() } /// Dönem içindeki bir hafta. Son gün dahildir (`last`), gösterim kolaylığı için. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Week { /// 1'den başlar pub index: u32, pub start: Date, pub last: Date, pub days: u32, } /// Dönemi Pazartesi başlangıçlı haftalara böler. /// Baştaki ve sondaki kısmi haftalar ayrı hafta sayılır (KAKEIBO_PROMPT §6). pub fn weeks(start: Date, end_exclusive: Date) -> Vec { let mut out = Vec::new(); let mut cursor = start; while cursor < end_exclusive { // Bir sonraki Pazartesi (ya da dönem sonu, hangisi önce gelirse) let to_monday = 7 - i64::from(cursor.weekday().number_days_from_monday()); let next = (cursor + Duration::days(to_monday)).min(end_exclusive); out.push(Week { index: out.len() as u32 + 1, start: cursor, last: next - Duration::days(1), days: days_between(cursor, next) as u32, }); cursor = next; } out } /// Kullanıcı ad vermezse dönem adı: /// - takvim ayının tamamıysa: `"Aralık 2026"` /// - değilse: `"15 Ara 2026 – 14 Oca 2027"` pub fn default_period_name(start: Date, end_exclusive: Date) -> String { let last = end_exclusive - Duration::days(1); let full_month = start.day() == 1 && last.month() == start.month() && last.year() == start.year() && (last + Duration::days(1)).day() == 1; if full_month { return format!("{} {}", MONTHS[month_idx(start)], start.year()); } format!( "{} {} {} – {} {} {}", start.day(), MONTHS_SHORT[month_idx(start)], start.year(), last.day(), MONTHS_SHORT[month_idx(last)], last.year() ) } fn month_idx(d: Date) -> usize { u8::from(d.month()) as usize - 1 } #[cfg(test)] mod tests { use super::*; fn d(s: &str) -> Date { parse_date(s).unwrap() } #[test] fn parse_and_format() { assert_eq!(format_date(d("2026-12-09")), "2026-12-09"); for bad in [ "2026-02-30", "2026-13-01", "26-12-09", "2026/12/09", "", "2026-12-9", ] { assert!(parse_date(bad).is_none(), "{bad} geçersiz olmalı"); } assert!(parse_date("2028-02-29").is_some(), "artık yıl"); } #[test] fn december_2026_weeks() { // 1 Aralık 2026 Salı → ilk hafta 6 gün (Sal–Paz), son hafta 4 gün (Pzt–Per) let w = weeks(d("2026-12-01"), d("2027-01-01")); let summary: Vec<(String, String, u32)> = w .iter() .map(|w| (format_date(w.start), format_date(w.last), w.days)) .collect(); assert_eq!( summary, vec![ ("2026-12-01".into(), "2026-12-06".into(), 6), ("2026-12-07".into(), "2026-12-13".into(), 7), ("2026-12-14".into(), "2026-12-20".into(), 7), ("2026-12-21".into(), "2026-12-27".into(), 7), ("2026-12-28".into(), "2026-12-31".into(), 4), ] ); assert_eq!(w.iter().map(|w| w.days).sum::(), 31); assert_eq!(w.last().unwrap().index, 5); } #[test] fn weeks_edge_cases() { // Pazartesi başlayan, tam 2 hafta let w = weeks(d("2026-11-30"), d("2026-12-14")); assert_eq!(w.len(), 2); assert!(w.iter().all(|w| w.days == 7)); // Tek gün (Pazar) let w = weeks(d("2026-12-06"), d("2026-12-07")); assert_eq!((w.len(), w[0].days), (1, 1)); // Boş aralık assert!(weeks(d("2026-12-07"), d("2026-12-07")).is_empty()); } #[test] fn period_names() { assert_eq!( default_period_name(d("2026-12-01"), d("2027-01-01")), "Aralık 2026" ); assert_eq!( default_period_name(d("2027-02-01"), d("2027-03-01")), "Şubat 2027" ); assert_eq!( default_period_name(d("2026-12-15"), d("2027-01-15")), "15 Ara 2026 – 14 Oca 2027" ); // Ayın 1'inde başlayıp ay ortasında biten assert_eq!( default_period_name(d("2026-12-01"), d("2026-12-16")), "1 Ara 2026 – 15 Ara 2026" ); } }