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>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user