/** * Para birimi yardımcıları — tutarlar HER ZAMAN tamsayı kuruştur (`amountMinor`). * 125,50 TL → 12550. Float ile hesap yapılmaz; yuvarlama yalnızca gösterimde. * * Ayrıştırma kuralları (tr-TR): * - Virgül varsa ondalık ayırıcıdır; noktalar ve boşluklar binlik ayırıcıdır: "1.250,50" → 125050. * - Virgül yoksa ve noktalar geçerli binlik gruplarıysa (1.250 / 12.345.678) binlik ayırıcıdır. * - Virgül yoksa ve tek nokta 1–2 ondalık içeriyorsa ondalıktır: "12.5" → 1250 (telefon klavyeleri). * - En fazla 2 ondalık; "₺", "TL", boşluklar ve baştaki +/− kabul edilir. Belirsiz girdi → null. */ export const MAX_AMOUNT_MINOR = 100_000_000_000_00; // 100 milyar TL üst sınırı (güvenli tamsayı içinde) const THOUSANDS_GROUPED = /^\d{1,3}(\.\d{3})+$/; export function parseMoney(input: string): number | null { let s = input .trim() .replace(/\s| | /g, "") .replace(/₺|TL$/i, ""); if (!s) return null; let sign = 1; if (s.startsWith("-") || s.startsWith("−")) { sign = -1; s = s.slice(1); } else if (s.startsWith("+")) { s = s.slice(1); } s = s.replace(/₺/g, ""); let intPart: string; let fracPart = ""; if (s.includes(",")) { const parts = s.split(","); if (parts.length !== 2) return null; [intPart, fracPart] = parts as [string, string]; if (intPart.includes(".")) { if (!THOUSANDS_GROUPED.test(intPart)) return null; intPart = intPart.replace(/\./g, ""); } } else if (s.includes(".")) { if (THOUSANDS_GROUPED.test(s)) { intPart = s.replace(/\./g, ""); } else { const parts = s.split("."); if (parts.length !== 2 || parts[1]!.length > 2) return null; [intPart, fracPart] = parts as [string, string]; } } else { intPart = s; } if (intPart === "") intPart = "0"; if (!/^\d+$/.test(intPart) || !/^\d{0,2}$/.test(fracPart)) return null; const minor = Number(intPart) * 100 + Number(fracPart.padEnd(2, "0")); if (!Number.isSafeInteger(minor) || minor > MAX_AMOUNT_MINOR) return null; return sign * minor === 0 ? 0 : sign * minor; } /** Form alanı için: 125050 → "1.250,50" (simgesiz). */ export function formatMoneyInput(amountMinor: number): string { assertMinor(amountMinor); const sign = amountMinor < 0 ? "-" : ""; const abs = Math.abs(amountMinor); const int = Math.trunc(abs / 100) .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, "."); const frac = (abs % 100).toString().padStart(2, "0"); return `${sign}${int},${frac}`; } export function assertMinor(value: number): void { if (!Number.isSafeInteger(value)) throw new TypeError(`Kuruş tutarı tamsayı olmalı: ${value}`); } /** Kuruş tutarlarını toplar (tamsayı garantili). */ export function sumMoney(values: readonly number[]): number { let total = 0; for (const v of values) { assertMinor(v); total += v; } assertMinor(total); return total; } /** * Oransal bölme (en büyük kalan yöntemi): kuruş kaybı olmadan `totalMinor`'u ağırlıklara böler. * Parçaların toplamı her zaman tam olarak `totalMinor`'dur. Taksit ve bölünmüş işlem için. * Eşit kalanlarda önceki parça önceliklidir (deterministik). */ export function allocate(totalMinor: number, weights: readonly number[]): number[] { assertMinor(totalMinor); if (weights.length === 0) throw new RangeError("En az bir ağırlık gerekli"); if (weights.some((w) => !Number.isFinite(w) || w < 0)) throw new RangeError("Ağırlıklar negatif olamaz"); const weightSum = weights.reduce((a, b) => a + b, 0); if (weightSum <= 0) throw new RangeError("Ağırlıkların toplamı sıfırdan büyük olmalı"); const sign = totalMinor < 0 ? -1 : 1; const abs = Math.abs(totalMinor); const raw = weights.map((w) => (abs * w) / weightSum); const parts = raw.map((r) => Math.floor(r)); let remainder = abs - parts.reduce((a, b) => a + b, 0); const order = raw .map((r, i) => ({ i, frac: r - Math.floor(r) })) .sort((a, b) => b.frac - a.frac || a.i - b.i); for (let k = 0; remainder > 0; k = (k + 1) % order.length, remainder--) parts[order[k]!.i]! += 1; return parts.map((p) => (p === 0 ? 0 : sign * p)); } /** Eşit bölme: 100,00 TL / 3 → [3334, 3333, 3333]. */ export function splitEvenly(totalMinor: number, count: number): number[] { if (!Number.isInteger(count) || count < 1) throw new RangeError("Parça sayısı en az 1 olmalı"); return allocate( totalMinor, Array.from({ length: count }, () => 1), ); }