95231cd4d6
- /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>
69 lines
2.6 KiB
Rust
69 lines
2.6 KiB
Rust
//! 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);
|
||
}
|
||
}
|