3f60efdc33
- TCMB (döviz alış kuru) ve xaus.com (altın) saatlik arka planda çekilir, gün başına saklanır - Kaynak erişilemezse son kur geçerli; altın için son USD kuru kullanılır - Elle kur girişi/silme; aynı gün elle girilen kur önceliklidir - GET /portfolio: güncel değer, kâr/zarar, toplamlar, son kur çekimi - RATES_REFRESH_MINUTES ayarı Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
5.8 KiB
Rust
182 lines
5.8 KiB
Rust
//! 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<Date> {
|
||
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<Week> {
|
||
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::<u32>(), 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"
|
||
);
|
||
}
|
||
}
|