fix(defter): haftalar ayın günlerine göre (1–8, 9–15, 16–22, 23–son), defter bugünün haftasıyla açılır

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 20:31:15 +03:00
parent 233907e3d9
commit 6e1000afd2
15 changed files with 144 additions and 130 deletions
+76 -29
View File
@@ -52,20 +52,35 @@ pub struct Week {
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).
/// Dönemi **ayın günlerine göre** haftalara böler (kullanıcı kararı, 2026-11-13):
/// 1–8 → 1. hafta, 9–15 → 2., 16–22 → 3., 23–son gün → 4. hafta.
///
/// Gün numarası dönem başından sayılır (dönem 15'inde başlıyorsa 15–22 = 1. hafta).
/// Genel kural: hafta sayısı `max(1, gün / 7)`; ilk hafta 8 gün, aradakiler 7 gün,
/// son hafta kalan günler (6–12 gün). 28–31 günlük dönem → daima 4 hafta.
/// Bütçe haftalara gün sayısıyla orantılı dağıtılır (`days`).
pub fn weeks(start: Date, end_exclusive: Date) -> Vec<Week> {
let mut out = Vec::new();
let total = days_between(start, end_exclusive);
if total <= 0 {
return Vec::new();
}
let count = (total / 7).max(1);
let mut out = Vec::with_capacity(count as usize);
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);
for i in 0..count {
let len = if i == count - 1 {
days_between(cursor, end_exclusive)
} else if i == 0 {
8
} else {
7
};
let next = cursor + Duration::days(len);
out.push(Week {
index: out.len() as u32 + 1,
index: i as u32 + 1,
start: cursor,
last: next - Duration::days(1),
days: days_between(cursor, next) as u32,
days: len as u32,
});
cursor = next;
}
@@ -123,37 +138,69 @@ mod tests {
assert!(parse_date("2028-02-29").is_some(), "artık yıl");
}
fn summary(w: &[Week]) -> Vec<(u32, String, String, u32)> {
w.iter()
.map(|w| (w.index, format_date(w.start), format_date(w.last), w.days))
.collect()
}
#[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();
fn month_weeks_follow_day_of_month() {
// Kasım 2026 (1'i Pazar): 1–8, 9–15, 16–22, 23–30 → 4 hafta
let w = weeks(d("2026-11-01"), d("2026-12-01"));
assert_eq!(
summary,
summary(&w),
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),
(1, "2026-11-01".into(), "2026-11-08".into(), 8),
(2, "2026-11-09".into(), "2026-11-15".into(), 7),
(3, "2026-11-16".into(), "2026-11-22".into(), 7),
(4, "2026-11-23".into(), "2026-11-30".into(), 8),
]
);
assert_eq!(w.iter().map(|w| w.days).sum::<u32>(), 31);
assert_eq!(w.last().unwrap().index, 5);
// 31 gün → son hafta 23–31 (9 gün); Şubat → 23–28 (6 gün); artık yıl → 7 gün
for (start, end, last_days) in [
("2026-12-01", "2027-01-01", 9),
("2027-02-01", "2027-03-01", 6),
("2028-02-01", "2028-03-01", 7),
] {
let w = weeks(d(start), d(end));
assert_eq!(w.len(), 4, "{start}");
assert_eq!(w[3].start.day(), 23, "{start}");
assert_eq!(w[3].days, last_days, "{start}");
assert_eq!(
w.iter().map(|w| i64::from(w.days)).sum::<i64>(),
days_between(d(start), d(end))
);
}
}
#[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)
// Ay ortasında başlayan dönem: günler dönem başından sayılır
let w = weeks(d("2026-12-15"), d("2027-01-15"));
assert_eq!(
summary(&w)
.iter()
.map(|x| (x.1.as_str(), x.3))
.collect::<Vec<_>>(),
vec![
("2026-12-15", 8),
("2026-12-23", 7),
("2026-12-30", 7),
("2027-01-06", 9)
]
);
// Kısa dönemler: 13 gün → tek hafta, 14 gün → 8 + 6
assert_eq!(summary(&weeks(d("2026-12-01"), d("2026-12-14"))).len(), 1);
let w = weeks(d("2026-12-01"), d("2026-12-15"));
assert_eq!((w[0].days, w[1].days), (8, 6));
// Tek gün
let w = weeks(d("2026-12-06"), d("2026-12-07"));
assert_eq!((w.len(), w[0].days), (1, 1));
// Uzun dönem (62 gün) → 8 hafta, son hafta ≤ 12 gün
let w = weeks(d("2026-11-01"), d("2027-01-02"));
assert_eq!(w.len(), 8);
assert!(w.iter().all(|w| (6..=12).contains(&w.days)));
// Boş aralık
assert!(weeks(d("2026-12-07"), d("2026-12-07")).is_empty());
}
+13 -19
View File
@@ -139,7 +139,7 @@ async fn monthly_budget_is_split_into_weeks_by_days() {
// Başta hepsi 0
let (_, b) = app.get(&url, &s).await;
assert_eq!(b.as_array().unwrap().len(), 4);
assert_eq!(week_amounts(group(&b, "needs")), vec![0; 5]);
assert_eq!(week_amounts(group(&b, "needs")), vec![0; 4]);
let (st, b) = app
.put(
@@ -151,12 +151,9 @@ async fn monthly_budget_is_split_into_weeks_by_days() {
assert_eq!(st, StatusCode::OK, "{b}");
let needs = group(&b, "needs");
assert_eq!(needs["monthly"], 100_000);
// Aralık: 6,7,7,7,4 gün → toplam tam 100.000
assert_eq!(
week_amounts(needs),
vec![19_355, 22_581, 22_581, 22_580, 12_903]
);
assert_eq!(week_amounts(group(&b, "culture")), vec![6, 7, 7, 7, 4]);
// Aralık: 1–8, 9–15, 16–22, 23–31 → 8,7,7,9 gün; toplam tam 100.000
assert_eq!(week_amounts(needs), vec![25_806, 22_581, 22_581, 29_032]);
assert_eq!(week_amounts(group(&b, "culture")), vec![8, 7, 7, 9]);
for g in b.as_array().unwrap() {
assert_eq!(
week_amounts(g).iter().sum::<i64>(),
@@ -207,20 +204,17 @@ async fn manual_week_override_redistributes_the_rest() {
.await;
let weeks_url = format!("/periods/{pid}/budget/weeks");
// 2. hafta (7–13 Ara) elle 50.000 → kalan 50.000 diğer haftalara (6+7+7+4 = 24 gün)
// 2. hafta (9–15 Ara) elle 50.000 → kalan 50.000 diğer haftalara (8+7+9 = 24 gün)
let (st, b) = app
.put(
&weeks_url,
&s,
json!({ "group": "needs", "week_start": "2026-12-07", "amount": 50_000 }),
json!({ "group": "needs", "week_start": "2026-12-09", "amount": 50_000 }),
)
.await;
assert_eq!(st, StatusCode::OK, "{b}");
let needs = group(&b, "needs");
assert_eq!(
week_amounts(needs),
vec![12_500, 50_000, 14_584, 14_583, 8_333]
);
assert_eq!(week_amounts(needs), vec![16_667, 50_000, 14_583, 18_750]);
assert_eq!(week_amounts(needs).iter().sum::<i64>(), 100_000);
let manual: Vec<bool> = needs["weeks"]
.as_array()
@@ -228,7 +222,7 @@ async fn manual_week_override_redistributes_the_rest() {
.iter()
.map(|w| w["is_manual"].as_bool().unwrap())
.collect();
assert_eq!(manual, vec![false, true, false, false, false]);
assert_eq!(manual, vec![false, true, false, false]);
assert_eq!(needs["weekly_over"], false);
// Aylık tutar değişince elle girilen hafta korunur
@@ -247,25 +241,25 @@ async fn manual_week_override_redistributes_the_rest() {
.put(
&weeks_url,
&s,
json!({ "group": "needs", "week_start": "2026-12-14", "amount": 30_000 }),
json!({ "group": "needs", "week_start": "2026-12-16", "amount": 30_000 }),
)
.await;
let needs = group(&b, "needs");
assert_eq!(needs["weekly_over"], true);
assert_eq!(week_amounts(needs), vec![0, 50_000, 30_000, 0, 0]);
assert_eq!(week_amounts(needs), vec![0, 50_000, 30_000, 0]);
// null → otomatiğe dön
app.put(
&weeks_url,
&s,
json!({ "group": "needs", "week_start": "2026-12-14", "amount": null }),
json!({ "group": "needs", "week_start": "2026-12-16", "amount": null }),
)
.await;
let (_, b) = app
.put(
&weeks_url,
&s,
json!({ "group": "needs", "week_start": "2026-12-07", "amount": null }),
json!({ "group": "needs", "week_start": "2026-12-09", "amount": null }),
)
.await;
assert_eq!(week_amounts(group(&b, "needs")).iter().sum::<i64>(), 74_000);
@@ -372,7 +366,7 @@ async fn budget_closed_period_and_isolation() {
app.put(
&format!("/periods/{pid}/budget/weeks"),
&b,
json!({ "group": "needs", "week_start": "2026-12-07", "amount": 1 }),
json!({ "group": "needs", "week_start": "2026-12-09", "amount": 1 }),
)
.await
.0,
+9 -12
View File
@@ -495,11 +495,11 @@ async fn actual_spending_in_summary_and_budget() {
)
.await;
// Aralık 2026 haftaları: 1–6 · 7–13 · 14–20 · 21–27 · 28–31
// Aralık 2026 haftaları: 1–8 · 9–15 · 16–22 · 23–31
for (date, amount, cat) in [
("2026-12-01", 1_000, &needs),
("2026-12-06", 2_000, &needs), // 1. haftanın son günü
("2026-12-07", 500, &needs), // 2. haftanın ilk günü
("2026-12-08", 2_000, &needs), // 1. haftanın son günü
("2026-12-09", 500, &needs), // 2. haftanın ilk günü
("2026-12-31", 300, &wants), // dönemin son günü
] {
let (st, b) = add(
@@ -516,7 +516,7 @@ async fn actual_spending_in_summary_and_budget() {
&app,
&s,
&pid,
json!({ "date": "2026-12-08", "amount": 400, "category_id": needs }),
json!({ "date": "2026-12-10", "amount": 400, "category_id": needs }),
)
.await;
app.patch(
@@ -530,7 +530,7 @@ async fn actual_spending_in_summary_and_budget() {
&app,
&s,
&pid,
json!({ "date": "2026-12-15", "amount": 700, "category_id": culture }),
json!({ "date": "2026-12-16", "amount": 700, "category_id": culture }),
)
.await;
let gone_id = gone["id"].as_str().unwrap();
@@ -546,18 +546,15 @@ async fn actual_spending_in_summary_and_budget() {
let (st, b) = app.get(&format!("/periods/{pid}/budget"), &s).await;
assert_eq!(st, StatusCode::OK);
assert_eq!(
spent_by_week(&b, "needs"),
(3_500, vec![3_000, 500, 0, 0, 0])
);
assert_eq!(spent_by_week(&b, "wants"), (700, vec![0, 400, 0, 0, 300]));
assert_eq!(spent_by_week(&b, "culture"), (0, vec![0; 5]));
assert_eq!(spent_by_week(&b, "needs"), (3_500, vec![3_000, 500, 0, 0]));
assert_eq!(spent_by_week(&b, "wants"), (700, vec![0, 400, 0, 300]));
assert_eq!(spent_by_week(&b, "culture"), (0, vec![0; 4]));
// Geri al → yeniden sayılır
app.post(&format!("/entries/{gone_id}/restore"), &s, json!({}))
.await;
let (_, b) = app.get(&format!("/periods/{pid}/budget"), &s).await;
assert_eq!(spent_by_week(&b, "culture"), (700, vec![0, 0, 700, 0, 0]));
assert_eq!(spent_by_week(&b, "culture"), (700, vec![0, 0, 700, 0]));
// Harcanabilir aşılırsa kalan negatif
add(
+1 -5
View File
@@ -40,11 +40,7 @@ async fn open_period_returns_weeks_and_default_name() {
.iter()
.map(|w| w["days"].as_i64().unwrap())
.collect();
assert_eq!(
days,
vec![6, 7, 7, 7, 4],
"Pazartesi başlangıçlı, kısmi haftalar ayrı"
);
assert_eq!(days, vec![8, 7, 7, 9], "1–8, 9–15, 16–22, 23–31");
let (_, cur) = app.get("/periods/current", &s).await;
assert_eq!(cur["id"], p["id"]);
+7 -7
View File
@@ -95,11 +95,11 @@ async fn four_questions_and_breakdowns() {
let culture = category(&app, &s, "culture", 0).await;
// Aralık 2026 haftaları: 1–6 · 7–13 · 14–20 · 21–27 · 28–31
spend(&app, &s, &pid, "2026-12-01", 100_000, &market).await;
spend(&app, &s, &pid, "2026-12-08", 50_000, &market).await;
spend(&app, &s, &pid, "2026-12-08", 300_000, &rent).await;
spend(&app, &s, &pid, "2026-12-15", 40_000, &wants).await;
spend(&app, &s, &pid, "2026-12-09", 50_000, &market).await;
spend(&app, &s, &pid, "2026-12-09", 300_000, &rent).await;
spend(&app, &s, &pid, "2026-12-16", 40_000, &wants).await;
spend(&app, &s, &pid, "2026-12-31", 10_000, &culture).await;
let gone = spend(&app, &s, &pid, "2026-12-15", 99_999, &wants).await;
let gone = spend(&app, &s, &pid, "2026-12-16", 99_999, &wants).await;
app.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s)
.await;
@@ -145,13 +145,13 @@ async fn four_questions_and_breakdowns() {
// Haftalar: 4 grubun toplamı; planlananların toplamı aylık bütçelerin toplamı
assert_eq!(
column(&r["weeks"], "spent"),
vec![100_000, 350_000, 40_000, 0, 10_000]
vec![100_000, 350_000, 40_000, 10_000]
);
assert_eq!(
column(&r["weeks"], "planned").iter().sum::<i64>(),
1_800_000
);
assert_eq!(r["weeks"][4]["last_date"], "2026-12-31");
assert_eq!(r["weeks"][3]["last_date"], "2026-12-31");
app.cleanup().await;
}
@@ -176,7 +176,7 @@ async fn overspending_gives_negative_savings() {
);
// Hiç bütçe yoksa: gruplar 0 planlı, haftalar yine dönem takviminden
assert_eq!(column(&r["groups"], "planned"), vec![0; 4]);
assert_eq!(r["weeks"].as_array().unwrap().len(), 5);
assert_eq!(r["weeks"].as_array().unwrap().len(), 4);
app.cleanup().await;
}