Files
kakeibo/backend/src/services/money.rs
T
suphicakir 95231cd4d6 feat(bütçe): aylık/haftalık bütçe, bütçe önerisi ve dönem özeti (backend)
- /api/periods/{id}/summary: gelir − sabit gider − birikim − devreden borç + artı = harcanabilir
- /api/periods/{id}/budget: 4 grup aylık; haftalar gün sayısına göre, toplam kuruşu kuruşuna eşit
- Elle hafta ayarı (kalan diğer haftalara dağılır), otomatiğe dönüş, aşım uyarısı
- Öneri: varsayılan yüzdeler / önceki dönem / son 3 dönem ortalaması
- Testler: orantılı bölme (3), bütçe entegrasyon (6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 03:42:49 +03:00

69 lines
2.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Para hesapları (kuruş, `i64`). Float yok.
/// `total` kuruşu ağırlıklara orantılı böler; **toplam her zaman `total`'a eşittir**.
///
/// "En büyük kalan" yöntemi: herkes tam payını alır, artan kuruşlar
/// küsuratı en büyük olana (eşitlikte öndekine) birer birer verilir.
/// Ağırlıkların hepsi 0 ise hepsine 0 döner (ve `total` dağıtılamaz → çağıran kontrol eder).
pub fn split_proportional(total: i64, weights: &[u64]) -> Vec<i64> {
let sum: u128 = weights.iter().map(|&w| u128::from(w)).sum();
if sum == 0 || total <= 0 {
return vec![0; weights.len()];
}
let total_u = total as u128;
let mut parts: Vec<i64> = Vec::with_capacity(weights.len());
let mut remainders: Vec<(u128, usize)> = Vec::with_capacity(weights.len());
for (i, &w) in weights.iter().enumerate() {
let exact = total_u * u128::from(w);
parts.push((exact / sum) as i64);
remainders.push((exact % sum, i));
}
let mut left = total - parts.iter().sum::<i64>();
// Büyük kalan önce; eşitlikte küçük indeks önce
remainders.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
for (_, i) in remainders {
if left == 0 {
break;
}
parts[i] += 1;
left -= 1;
}
parts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sums_exactly_and_is_proportional() {
// 1.000,00 ₺ Aralık haftalarına (6,7,7,7,4 gün)
let p = split_proportional(100_000, &[6, 7, 7, 7, 4]);
assert_eq!(p.iter().sum::<i64>(), 100_000);
assert_eq!(p, vec![19_355, 22_581, 22_581, 22_580, 12_903]);
}
#[test]
fn default_percentages() {
// Tam paylar 617.283,5 / 370.370,1 / 123.456,7 / 123.456,7 → artan 2 kuruş
// küsuratı en büyük olan ikisine (0,7'ler) gider
assert_eq!(
split_proportional(1_234_567, &[50, 30, 10, 10]),
vec![617_283, 370_370, 123_457, 123_457]
);
assert_eq!(split_proportional(1, &[50, 30, 10, 10]), vec![1, 0, 0, 0]);
}
#[test]
fn edge_cases() {
assert_eq!(split_proportional(0, &[1, 2]), vec![0, 0]);
assert_eq!(split_proportional(-500, &[1, 2]), vec![0, 0]);
assert_eq!(split_proportional(100, &[0, 0]), vec![0, 0]);
assert_eq!(split_proportional(100, &[]), Vec::<i64>::new());
assert_eq!(split_proportional(7, &[0, 3, 0]), vec![0, 7, 0]);
// Çok büyük tutarlarda taşma yok
let big = 10_000_000_000_000;
assert_eq!(split_proportional(big, &[31, 31]).iter().sum::<i64>(), big);
}
}