Mundarija (20)
- Bu darsda
- 1. Nega bu kerak?
- 2. Asosiy qoida: tiyinda butun son
- 2.1 Satrdan tiyinga — float'siz
- 3. Katta sonlar va BigInt
- 4. Yaxlitlash: qaysi qoida?
- 5. Bo'lish: tiyin yo'qolmasin
- 6. Kutubxonalar: decimal.js, big.js, dinero.js
- 7. Valyuta va kurs
- 8. Ekranga chiqarish: Intl.NumberFormat
- 9. Bazadan kelgan pul: pg va Postgres turlari
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): tiyindanSom
- 3-mashq (qiyin): chekdagi QQS
- 12. Loyiha qadami: pul.ts moduli
- 13. Real ishda
- Xulosa
- Manbalar
Pul bilan ishlash JS'da: tiyin, BigInt va yaxlitlash
Qisqacha:
0.1 + 0.2JavaScript'da0.3emas. Pulni kasrli son (float) bilan hisoblasangiz, tiyinlar jimgina yo'qoladi yoki paydo bo'ladi. Qoida oddiy: summani tiyinda butun son sifatida saqlang va hisoblang, satrdan tiyinga float'siz o'tkazing, foiz va bo'lishda yaxlitlash qoidasini o'zingiz tanlang,Intl.NumberFormatni faqat ko'rsatish uchun ishlating. Bu darsda kanonpul.tsmoduli va uning beshta testi shu qoidalarni amalga oshiradi.
Bu darsda
- Float xatolarini kanon o'lchovi bilan ko'rasiz va nega pulga yaramasligini tushunasiz.
- Summani satrdan tiyinga float'siz o'tkazasiz.
Numberchegarasi,BigIntva JSON muammosini bilasiz.- Half-even yaxlitlash bilan foiz hisoblaysiz, summani tiyin yo'qotmasdan bo'lasiz.
- Kutubxonalarni (decimal.js, big.js, dinero.js) o'lchov bilan solishtirasiz.
- Postgres'dan kelgan pul qiymatlarini
pgqanday qaytarishini bilasiz. - To'lov moduli uchun
pul.tsni yozasiz.
Oldin bilishingiz kerak: Number turi JS'da, BigInt, Intl.NumberFormat, Pul, miqdor va enum'larni modellash, node-postgres (pg) asoslari.
Oldingi darsda Telegram Stars bilan birinchi to'lovni qabul qildik. Endi to'lov blokiga o'tamiz: Payme, Click, Uzum, Stripe, obuna, webhook va hisob-kitob. Hammasi bitta poydevorga tayanadi — pulni to'g'ri saqlash va hisoblash. Shu poydevordan boshlaymiz.
1. Nega bu kerak?
Internet-do'kon 19.99 dollarlik kitob sotadi. Dasturchi uni sentga o'giradi: 19.99 * 100. Natija 1999 bo'lishi kerak, lekin JavaScript boshqacha javob beradi. Kanon tajribasi buni Node 24.21.0 da o'lchadi. Siz ham brauzerda ko'ring:
console.log(0.1 + 0.2);
console.log(19.99 * 100);
console.log(1.005 * 100);
console.log((1.005).toFixed(2));
let s = 0;
for (let i = 0; i < 1000; i++) s += 0.1;
console.log(s);
console.log(0.1 + 0.2 === 0.3);Konsolda:
0.30000000000000004
1998.9999999999998
100.49999999999999
1.00
99.9999999999986
falseHar qatorni pul tilida o'qiymiz:
19.99 * 100—1998.9999….Math.floorbilan kesilsa, mijozdan 1 998 sent olinadi, bitta sent yo'qoladi.1.005-bob.toFixed(2)—"1.00". Maktabda o'rgangan qoida bo'yicha 1.01 bo'lishi kerak edi.1.005xotirada aslida1.00499999…bo'lib turadi.- 0.1 ni ming marta qo'shdik — 100 emas,
99.9999999999986. Kunlik minglab to'lov qo'shilsa, kun oxiridagi hisobot bankdagi summaga mos kelmaydi.
Sababi Number turi JS'da darsidan tanish: number — IEEE 754 standartidagi ikkilik kasr. 0.1 ni ikkilikda aniq yozib bo'lmaydi, xuddi 1/3 ni o'nlikda aniq yozib bo'lmaganidek (0.3333…).
O'xshatish: chekka do'kondagi tarozi. Har tortishda 0.001 gramm xato sezilmaydi. Lekin omborchi kun bo'yi minglab qop tortsa, kechqurun bir necha kilogramm "yo'qoladi". Pul hisobida bitta tiyin farq ham — buxgalteriyada xato, auditda savol.
Diqqat: Bu xato JavaScript'ga xos emas. Python, Java, C# — float bilan hamma tilda shunday. Farqi shuki, JavaScript'da butun son uchun alohida tur yo'q:
numberdoim float.
2. Asosiy qoida: tiyinda butun son
To'lov tizimlarining deyarli hammasi pulni eng kichik birlikda (minor unit) kutadi: Payme va Uzum — tiyinda, Stripe — valyutaning eng kichik birligida (dollar uchun sentda). Stripe hujjatidagi nol kasrli valyutalar ro'yxatida UZS yo'q, ya'ni u yerda ham so'm ikki kasrli — tiyinda (2026-10-28 holatiga). 12 500 so'm = 1 250 000 tiyin. Butun sonlarda qo'shish, ayirish va ko'paytirish aniq: 1250000 + 50 hech qachon 1250049.9999 bo'lmaydi.
| Qayerda | Qanday |
|---|---|
| Bazada | bigint ustun, tiyinda (Pul, miqdor va enum'larni modellash) |
| Kodda | number (butun), Number.isSafeInteger bilan tekshirilgan |
| API'da | tiyinda butun son yoki so'mdagi satr ("12500.50") |
| Ekranda | Intl.NumberFormat — faqat ko'rsatish uchun |
Pul tizim ichida qanday yuradi — bitta rasmda:
flowchart LR
K["Kirish: '12 500,50'"] -->|somdanTiyin| T[1250050 tiyin]
T -->|foiz, taqsimla: BigInt| H[Hisob natijasi, tiyin]
H --> DB[(Postgres bigint)]
DB -->|pg: satr| H
H -->|Intl.NumberFormat| E["Ekran: 12 500,50 so'm"]
E -.->|qaytib o'qilmaydi| HKirish va chiqish — satr, ichkarida esa faqat butun tiyin. Ekrandagi matn bir tomonlama: undan hisobga qaytilmaydi.
Kanon modulda tur nomi ham shuni aytadi:
export type Tiyin = number;Bu TypeScript uchun shunchaki number taxallusi — kompilyator Tiyin va so'mni farqlamaydi. Lekin kod o'quvchi uchun aniq: bu yerda tiyin, butun son. tiyinmi(x) funksiyasi esa ishga tushganda tekshiradi: Number.isSafeInteger(x).
2.1 Satrdan tiyinga — float'siz
Foydalanuvchi formaga 12 500,50 yozadi. Click esa summani "12500.50" satri bilan yuboradi (keyingi darslarda ko'ramiz). Eng oson yo'l — parseFloat(s) * 100 — yana float'ga qaytadi. Kanon satrni to'g'ridan butun songa o'giradi: butun qism va kasr qismini alohida o'qiydi.
function somdanTiyin(kirish) {
const t = kirish.trim().replace(/[\s']/g, "");
const m = /^(-?)(\d+)(?:[.,](\d{1,2}))?$/.exec(t);
if (!m) throw new Error(`noto'g'ri summa: "${kirish}"`);
const [, ishora = "", butun = "0", kasr = ""] = m;
const n = Number(butun) * 100 + Number(kasr.padEnd(2, "0"));
if (!Number.isSafeInteger(n)) {
throw new Error(`summa juda katta: "${kirish}"`);
}
return ishora === "-" ? -n : n;
}
console.log(somdanTiyin("12 500"));
console.log(somdanTiyin("12500,5"));
console.log(somdanTiyin("0.01"));
console.log(somdanTiyin("-3.10"));
for (const x of ["12.345", "1,234.56", "1e3", ""]) {
try {
somdanTiyin(x);
} catch (e) {
console.log(e.message);
}
}
const uch = somdanTiyin("0.3");
console.log(somdanTiyin("0.1") + somdanTiyin("0.2") === uch);Konsolda:
1250000
1250050
1
-310
noto'g'ri summa: "12.345"
noto'g'ri summa: "1,234.56"
noto'g'ri summa: "1e3"
noto'g'ri summa: ""
trueQadamlar:
- Bo'shliq va
'olib tashlanadi — minglik ajratgich (12 500,12'500). JavaScript'da\suzilmas bo'shliqni (NBSP, U+00A0) ham ushlaydi. Bu muhim:Intlso'm summasini aynan NBSP bilan ajratadi (pastda ko'ramiz). - Regex faqat bitta kasr ajratgichiga (
.yoki,) va undan keyin 0–2 raqamga ruxsat beradi.12.345— uchinchi raqam tiyindan kichik, uni yaxlitlab "to'g'rilash" o'rniga rad etamiz. 1,234.56(amerikacha minglik vergul) ham rad. Vergul kasrmi yoki minglikmi — taxmin qilmaymiz."5"→"50"(padEnd):12500,5— 50 tiyin, 5 emas.1e3va0x10— JavaScriptNumber()ularni son deb qabul qiladi, regex esa yo'q.
Oxirgi qator: tiyinda 0.1 + 0.2 so'm aniq 0.3 so'm.
Tekshirib ko'ring: Nega
Number(butun) * 100float xatosini bermaydi,parseFloat("19.99") * 100esa beradi?
Javob
Number("19") — butun son, 19 * 100 = 1900 aniq. Butun sonlar 2^53 gacha float'da aniq ifodalanadi, ko'paytirish natijasi ham butun. parseFloat("19.99") esa ikkilikda aniq yozib bo'lmaydigan 19.99 ni beradi va xato ko'paytirishda ko'rinadi.
3. Katta sonlar va BigInt
number butun sonlarni faqat Number.MAX_SAFE_INTEGER gacha aniq saqlaydi (BigInt):
console.log(Number.MAX_SAFE_INTEGER);
const trln = Number.MAX_SAFE_INTEGER / 100 / 1e12;
console.log(trln.toFixed(1), "trln so'm");
console.log(2 ** 53 + 1);
console.log(2n ** 53n + 1n);
try {
JSON.stringify({ summa: 10n });
} catch (e) {
console.log(e.name, e.message);
}
console.log(JSON.stringify({ summa: String(10n) }));
console.log(7n / 2n, -7n / 2n);Konsolda:
9007199254740991
90.1 trln so'm
9007199254740992
9007199254740993n
TypeError Do not know how to serialize a BigInt
{"summa":"10"}
3n -3n- Tiyinda chegara — 90.1 trillion so'm. Bitta to'lov uchun bu yetarli. Lekin butun do'konning yillik aylanmasi yoki bank hisobi yig'indisi uchun xavfsizroq yo'l —
BigInt. 2 ** 53 + 1— bir tiyin jimgina yo'qoldi, xato ham chiqmadi.BigInt(nqo'shimchasi) esa to'g'ri natija berdi.JSON.stringifyBigIntni bilmaydi vaTypeErrortashlaydi. Yechim — API'da katta summani satr qilib yuborish ("10").BigIntbo'lishi kasrni tashlab yuboradi (nolga qarab):7n / 2n→3n,-7n / 2n→-3n. Bu yaxlitlash emas, kesish. Yaxlitlashni o'zimiz yozamiz.
Kanon modul shunday qaror qildi: bitta summa — number (tez, JSON bilan oson), foiz va ulushlar hisobi ichida — BigInt (oraliq ko'paytma 2^53 dan oshib ketmasin), yig'indilar (hisobot, ledger) — bazada bigint.
4. Yaxlitlash: qaysi qoida?
Komissiya, QQS (qo'shilgan qiymat solig'i), chegirma — hammasi foiz. Foiz esa ko'pincha kasr tiyin beradi: 150 tiyinning 1 foizi — 1.5 tiyin. 1 yoki 2? Bu biznes qarori, va uni kodda aniq yozish kerak.
console.log(Math.round(2.5), Math.round(3.5), Math.round(-2.5));Konsolda:
3 4 -2Math.round yarimni doim yuqoriga (musbat cheksizlik tomon) suradi: -2.5 → -2, 2.5 → 3. Manfiy summalarda (qaytarish) bu nosimmetrik: bir yo'nalishda doim bir tiyin ortiqcha.
Ikki keng tarqalgan qoida:
| Qoida | 0.5 / 1.5 / 2.5 / -1.5 | Qayerda |
|---|---|---|
| half-up (half-expand) | 1 / 2 / 3 / -2 | maktab qoidasi, Intl sukuti |
| half-even (bank) | 0 / 2 / 2 / -2 | buxgalteriya, IEEE 754, Python round |
Half-even (bank yaxlitlashi) — yarim holatda eng yaqin juft songa. Ko'p operatsiyada yuqoriga va pastga yaxlitlashlar teng uchraydi, yig'indida tizimli siljish bo'lmaydi.
Kanon foiz funksiyasi foizni basis point (bp, 1 % = 100 bp) bilan oladi — 2.5 % ni float'da emas, 250 butun soni bilan berish uchun:
function foiz(t, foizBp) {
const surat = BigInt(t) * BigInt(foizBp);
const maxraj = 10_000n;
const q = surat / maxraj;
const r = surat % maxraj;
const ikki = (r < 0n ? -r : r) * 2n;
let natija = q;
if (ikki > maxraj || (ikki === maxraj && q % 2n !== 0n)) {
natija += surat < 0n ? -1n : 1n;
}
return Number(natija);
}
console.log(foiz(10_000, 250));
console.log(foiz(50, 100), foiz(150, 100), foiz(250, 100));
console.log(foiz(-150, 100));
console.log(foiz(1_250_000, 120));Konsolda:
250
0 2 2
-2
15000Qanday ishlaydi:
surat = t × bp,maxraj = 10 000— natijasurat / maxraj. Ko'paytmaBigIntda: katta summa × bp ham aniq.q— butun qism (BigIntbo'lishi kesadi),r— qoldiq.ikki = |r| × 2nimaxrajbilan solishtiramiz: kattaroq — yarimdan ortiq, yuqoriga; teng — aynan yarim, faqatqtoq bo'lsa yuqoriga (juftga).- Manfiy summada yo'nalish ham manfiy.
Natijalar: 100 so'mning 2.5 % i — 250 tiyin. 0.5 → 0, 1.5 → 2, 2.5 → 2 — jadvaldagi half-even qatori. 12 500 so'mning 1.2 % i (1.2 % = 120 bp) — 150 so'm.
5. Bo'lish: tiyin yo'qolmasin
Uch do'st 100 so'mlik hisobni bo'lishadi. 100 / 3 = 33.33…. Har biriga 33.33 so'm berilsa, jami 99.99 — bir tiyin havoga uchdi. Bu xato "chek bo'lish", "bo'lib to'lash" va "marketplace'da sotuvchilar ulushi" kabi har joyda chiqadi.
Kanon taqsimla ulushlarni butun songa kesadi, so'ng qoldiqni birinchi ulushlarga bittadan tarqatadi:
function taqsimla(t, ulushlar) {
const jami = ulushlar.reduce((a, b) => a + b, 0);
const natija = ulushlar.map((u) =>
Number((BigInt(t) * BigInt(u)) / BigInt(jami)),
);
let qoldiq = t - natija.reduce((a, b) => a + b, 0);
for (let i = 0; qoldiq !== 0; i = (i + 1) % natija.length) {
natija[i] += Math.sign(qoldiq);
qoldiq -= Math.sign(qoldiq);
}
return natija;
}
console.log(taqsimla(100, [1, 1, 1]));
console.log(taqsimla(10_000_000, [1, 1, 1]));
console.log(taqsimla(-100, [1, 1, 1]));
console.log(taqsimla(1000, [70, 30]));
const q = taqsimla(1_234_567, [3, 5, 11, 2]);
console.log(q, q.reduce((a, b) => a + b, 0));Konsolda:
[ 34, 33, 33 ]
[ 3333334, 3333333, 3333333 ]
[ -34, -33, -33 ]
[ 700, 300 ]
[ 176367, 293945, 646678, 117577 ] 1234567100 / 3→ 34, 33, 33: jami aniq 100. Ortiqcha tiyin birinchiga tushdi.- 100 000 so'm (10 000 000 tiyin) uch kishiga — kanonda dinero.js
allocateham aynan shu javobni berdi: 33 333.34, 33 333.33, 33 333.33. - Ulushlar teng bo'lmasa ham ishlaydi: 70/30, yoki 3:5:11:2 — yig'indi doim boshlang'ich summa.
Kanon modulda bundan tashqari kirish tekshiruvi bor: bo'sh ro'yxat, manfiy yoki kasr ulush va yig'indisi 0 bo'lgan ulushlar — PulXato.
6. Kutubxonalar: decimal.js, big.js, dinero.js
Butun tiyin har doim ham qulay emas: kurs (12 650.37), foiz stavkasi (0.0425), ko'p valyuta. Buning uchun o'nlik kasr (decimal) kutubxonalari bor. Kanon uchtasini sinadi (decimal.js 10.6.0, big.js 7.0.1, dinero.js 2.0.2) — vazifa: 100 000 so'mni uch kishiga bo'lish va 12 % QQS:
decimal.js: 33333.33 ×3 = 99999.99 | QQS: 12000.00
big.js: 33333.33 | QQS ichida (narx/1.12*0.12): 10714.29
dinero.js allocate [1,1,1]: 33333.34, 33333.33, 33333.33 | add/multiply: 112000.0000Uchta saboq:
- decimal.js aniq hisobladi, lekin tiyinni yo'qotdi:
33333.33 × 3 = 99999.99. Kutubxona arifmetikani to'g'ri bajaradi, taqsimlash qoidasini esa bermaydi — u sizning ishingiz. - dinero.js pulga maxsus yozilgan:
allocateqoldiqni o'zi tarqatadi (bizningtaqsimlakabi). Lekinmultiplydan keyin natija112000.0000— scale o'sdi (2 dan 4 ga). Uni qayta 2 xonaga keltirish ham sizning qaroringiz. - Hech bir kutubxona "QQS narx ichidami yoki ustidanmi" savoliga javob bermaydi. 12 % ustidan — 12 000, ichida — 10 714.29 so'm. Bu soliq qoidasi, kod emas.
Tezlik — 200 000 ta qo'shish (kanon, bitta o'lchov, Node 24.21.0, i5-12500H):
number (tiyin) 200000 qo'shish: 1.0 ms
BigInt 200000 qo'shish: 2.8 ms
decimal.js 200000 qo'shish: 117.8 ms
big.js 200000 qo'shish: 71.9 ms
dinero.js 200000 qo'shish: 91.6 msButun tiyin — kutubxonalardan 70–120 marta tez. Bu farq hisobotda va provayder bilan solishtirishda (reconciliation, millionlab qator) seziladi, bitta to'lovda esa ahamiyatsiz.
Kanon tanlovi: to'lov moduli kutubxonasiz — tiyin number + BigInt ichki hisob. Kurs va foiz stavkalari ham satr ("12 650.37") yoki basis point sifatida keladi va BigInt bilan hisoblanadi. Agar loyihada ko'p valyuta va murakkab moliyaviy formulalar bo'lsa — dinero.js yoki decimal.js'ni o'ylab ko'ring, lekin yaxlitlash va taqsimlash qoidalarini baribir o'zingiz yozasiz.
7. Valyuta va kurs
Stripe'da narx dollarda, sizning hisobingiz so'mda. 19.99 dollar (1 999 sent) — kurs 12 650.37 so'm. Kanon sentdanTiyin kursni satrdan oladi (somdanTiyin bilan — float'siz), ko'paytiradi va half-even bilan yaxlitlaydi:
function somdanTiyin(s) {
const m = /^(\d+)(?:[.,](\d{1,2}))?$/.exec(s.replace(/[\s']/g, ""));
if (!m) throw new Error(`noto'g'ri summa: "${s}"`);
return Number(m[1]) * 100 + Number((m[2] ?? "").padEnd(2, "0"));
}
function sentdanTiyin(sent, kurs) {
const surat = BigInt(sent) * BigInt(somdanTiyin(kurs));
const q = surat / 100n;
const r = surat % 100n;
const yuqori = r * 2n > 100n || (r * 2n === 100n && q % 2n !== 0n);
return Number(yuqori ? q + 1n : q);
}
console.log(sentdanTiyin(1999, "12 650.37"));
console.log(19.99 * 12650.37);Konsolda:
25288090
252880.8963Aniq ko'paytma 252 880.8963 so'm — 25 288 089.63 tiyin, yaxlitlangani 25 288 090. Bu safar float ham to'g'ri ko'rindi — lekin bu omad: «Nega bu kerak?» bo'limidagi 19.99 * 100 ham "oddiy" ko'paytma edi. Qaysi son buziladi — oldindan bilib bo'lmaydi, shuning uchun qoidani hamma joyda bir xil qo'llaymiz. (Bu ixcham nusxada manfiy summa yo'q — kanon modul to'liq somdanTiyin ni ishlatadi.)
Amalda kurs qayerdan olinadi, qachon qotiriladi (buyurtma paytidami, to'lov paytidami) va bankning konvertatsiya komissiyasi kim zimmasida — bular ham biznes qarori. Bitta qoida: kursni buyurtma bilan birga saqlang — keyin hisobotda "qaysi kurs bilan hisoblangan edi?" degan savolga javob bo'lsin.
8. Ekranga chiqarish: Intl.NumberFormat
So'mni chiroyli ko'rsatish — Intl.NumberFormat ishi:
const korsat = (tiyin, til, kasr = 2) =>
new Intl.NumberFormat(til, {
style: "currency",
currency: "UZS",
minimumFractionDigits: kasr,
maximumFractionDigits: kasr,
})
.format(tiyin / 100)
.replace(/\s/g, " ");
console.log(korsat(1_250_050, "uz-Latn-UZ"));
console.log(korsat(1_250_050, "ru-RU"));
console.log(korsat(1_250_050, "en-US"));
console.log(korsat(1_250_050, "uz-Latn-UZ", 0));Konsolda:
12 500,50 soʻm
12 500,50 UZS
UZS 12,500.50
12 501 soʻmE'tibor bering:
uz-Latn-UZ— minglik bo'shliq, kasr vergul, valyuta nomisoʻm(o'zbek lotin alifbosidagi rasmiy tutuq belgisi — U+02BB, oddiy'emas). Bo'shliqlar aslida NBSP — chiqishni o'qishga qulay bo'lishi uchunreplacebilan oddiy bo'shliqqa almashtirdik. Kanon testi ham shunday qiladi. Shuning uchunsomdanTiyinham\sni olib tashlaydi.tiyin / 100— bu yerda float bor, lekin faqat ko'rsatish uchun. Hisob natijasi bu satrdan hech qachon qaytib olinmaydi.- Oxirgi qator: kasrsiz rejimda 12 500,50 → 12 501.
Intlning sukut yaxlitlashi —halfExpand(yarim — noldan uzoqqa). Ekranda 12 501, hisobda esa 1 250 050 tiyin. Agar chekda "jami 12 501 so'm" yozilsa va mijoz 12 501 to'lasa — farq 50 tiyin. Kasrsiz ko'rsatish kerak bo'lsa, summaning o'zini oldin biznes qoidasi bilan yaxlitlang.
Chiqish ICU (Unicode lokal ma'lumotlari) versiyasiga bog'liq. Kanon va biz Node 24.21.0 da tekshirdik. Brauzeringizdagi ICU boshqacha bo'lsa (masalan, UZS o'rniga soʻm), natija biroz farq qilishi mumkin. Shuning uchun testda Intl chiqishini qattiq solishtirish mo'rt — kanon testi bo'shliqlarni normallab solishtiradi.
9. Bazadan kelgan pul: pg va Postgres turlari
Kanon tolov bazasida summalar bigint. node-postgres (pg) asoslari darsida pg bigint ni satr qilib qaytarishini ko'rgansiz. Kanon to'rt turni bitta so'rovda sinadi (Postgres 18.6, pg 8.23.1):
{"b":"1250050","n":"12500.50","m":"$12,500.50","katta":"9007199254740993","f":0.30000000000000004,"n2":"0.3"} | turlar: b:string n:string m:string katta:string f:number n2:string
lc_monetary: en_US.utf8 — `money` turi locale'ga bog'liq, ishlatmang
setTypeParser(20 → BigInt): 9007199254740993n bigint | sum: 6n| Postgres turi | pg qaytaradi |
Pul uchun |
|---|---|---|
bigint |
satr "1250050" |
ha — tiyinda |
numeric |
satr "12500.50" |
ha — aniq o'nlik, lekin JS'da baribir satr |
float8 |
number 0.30000000000000004 |
yo'q — float |
money |
"$12,500.50" |
yo'q — format server sozlamasiga bog'liq |
bigint ni satr qilib qaytarish — pg ning ehtiyotkorligi: u 2^53 dan katta bo'lishi mumkin, number ga o'girsa, raqam buzilardi. Kanon ikki yo'l ko'rsatdi:
- Bitta summa:
Number(qator.summa)—Number.isSafeIntegerbilan tekshirib. - Hammasi
BigIntbo'lsin:types.setTypeParser(20, BigInt)(20 —bigintturining ichki raqami). Endisum(...)ham6nkabiBigIntqaytadi.
money turi ayniqsa xavfli: kanon serverida lc_monetary en_US edi va 12 500,50 so'm $12,500.50 bo'lib chiqdi — dollar belgisi bilan. Boshqa serverda boshqa format. Postgres hujjati ham uni yangi loyihalar uchun tavsiya qilmaydi.
Float muammosini o'z Postgres 17 klasterimizda ham ko'rsatamiz (biz ishga tushirdik, PostgreSQL 17.6):
SELECT 0.1::float8 + 0.2::float8 AS float8,
0.1::numeric + 0.2::numeric AS numeric,
1250050::bigint / 3 AS bolish,
1250050::bigint % 3 AS qoldiq; float8 | numeric | bolish | qoldiq
---------------------+---------+--------+--------
0.30000000000000004 | 0.3 | 416683 | 1
(1 row)Postgres'da ham float8 xatoli, numeric aniq. bigint bo'lishi JavaScript BigInt kabi kesadi va qoldiq qoladi — taqsimlash qoidasini SQL'da ham o'zingiz yozasiz.
10. Ko'p uchraydigan xatolar
| Xato | Oqibati | To'g'risi |
|---|---|---|
parseFloat(s) * 100 |
1998.9999…, tiyin yo'qoladi |
satrdan butun songa (somdanTiyin) |
toFixed(2) bilan yaxlitlash |
1.005 → "1.00" |
BigInt + aniq qoida (half-even) |
Math.round manfiy summada |
-2.5 → -2, nosimmetrik |
ishorani alohida hisoblash |
summani 3 ga bo'lib, har biriga toFixed(2) |
99.99, tiyin yo'qoladi | taqsimla — qoldiqni tarqatish |
JSON.stringify da BigInt |
TypeError |
API'da satr |
float8 yoki money ustun |
xato yoki locale formati | bigint (tiyin) yoki numeric |
Intl chiqishini qayta parse qilish |
NBSP, soʻm, yaxlitlangan qiymat |
ko'rsatish — bir tomonlama |
| kursni saqlamaslik | hisobotni qayta tiklab bo'lmaydi | kurs buyurtma bilan birga |
11. Mashqlar
Mashqlar kurs/mashqlar/29/27-pul/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
Math.round(19.99 * 100)to'g'ri natija beradi, demakparseFloatbilan ishlasa bo'ladi.BigIntbo'lishi7n / 2nnatijasini4nga yaxlitlaydi.- Half-even qoidasida 2.5 → 2, 3.5 → 4.
Intl.NumberFormatninguz-Latn-UZchiqishinisomdanTiyinqayta o'qiy oladi, shuning uchun uni hisobda ishlatsa bo'ladi.- Postgres
moneyturi so'm uchun to'g'ri tanlov.
Yechim
- Noto'g'ri — bu holda tasodifan to'g'ri, lekin
Math.roundhar doim ham qutqarmaydi (1.005 * 100→100.4999…→100, 101 emas). Satrdan butun songa o'ting. - Noto'g'ri —
BigIntbo'lishi kesadi:3n. - To'g'ri — yarim holatda eng yaqin juftga.
- Noto'g'ri — ekrandagi qiymat yaxlitlangan bo'lishi mumkin (12 501), u faqat ko'rsatish uchun.
- Noto'g'ri — format server sozlamasiga bog'liq (kanonda
$12,500.50); tiyindabigintishlating.
2-mashq (o'rta): tiyindanSom
Click va ba'zi API'lar summani so'mda, ikki xonali kasr bilan kutadi: "1000.00". Float'siz tiyindanSom(t) yozing: 100 005 → "1000.05", -5 → "-0.05", 0 → "0.00". Butun son bo'lmasa (1.5) — xato. Ishora: Math.abs, Math.floor(m / 100), m % 100 va padStart.
Yechim
function tiyindanSom(t) {
if (!Number.isSafeInteger(t)) {
throw new TypeError(`tiyin butun son emas: ${t}`);
}
const m = Math.abs(t);
const som = String(Math.floor(m / 100));
const qoldiq = String(m % 100).padStart(2, "0");
return `${t < 0 ? "-" : ""}${som}.${qoldiq}`;
}
console.log(tiyindanSom(100_005));
console.log(tiyindanSom(-5));
console.log(tiyindanSom(0));
console.log(tiyindanSom(1_250_050));
try {
tiyindanSom(1.5);
} catch (e) {
console.log(e.name, e.message);
}Konsolda:
1000.05
-0.05
0.00
12500.50
TypeError tiyin butun son emas: 1.5Ishorani alohida qo'yamiz: Math.floor(-5 / 100) -1 beradi va natija "-1.95" kabi buzilardi. Shuning uchun avval Math.abs, keyin ishora. Kanon tiyindanSom aynan shunday.
3-mashq (qiyin): chekdagi QQS
Chekdagi narxlar QQS ichida (12 %). Bitta qatorning QQS summasi — summa × 12 / 112. U half-even bilan tiyingacha yaxlitlanadi. qqsIchida(tiyin) yozing (BigInt bilan) va uchta tovarli chek uchun ikki usulni solishtiring: (a) har qator QQS ini hisoblab qo'shish, (b) jami summadan bir marta hisoblash. Tovarlar: kitob 15 000, daftar 28 000, ruchka 5 000 so'm. 100 000 so'm uchun natija kanondagi big.js bilan mos kelishini tekshiring (10 714.29 so'm).
Yechim
function halfEven(surat, maxraj) {
const q = surat / maxraj;
const r = surat % maxraj;
const ikki = (r < 0n ? -r : r) * 2n;
if (ikki > maxraj || (ikki === maxraj && q % 2n !== 0n)) {
return q + (surat < 0n ? -1n : 1n);
}
return q;
}
function qqsIchida(t, foiz = 12) {
const surat = BigInt(t) * BigInt(foiz);
return Number(halfEven(surat, BigInt(100 + foiz)));
}
const chek = [1_500_000, 2_800_000, 500_000];
const qatorlar = chek.map((t) => qqsIchida(t));
const jami = chek.reduce((a, b) => a + b, 0);
console.log(qatorlar);
console.log("qatorlardan:", qatorlar.reduce((a, b) => a + b, 0));
console.log("jamidan:", qqsIchida(jami));
console.log(qqsIchida(10_000_000));Konsolda:
[ 160714, 300000, 53571 ]
qatorlardan: 514285
jamidan: 514286
1071429Ikki usul bir tiyin farq qildi: har qatorni yaxlitlash xatolari qo'shilib ketadi. Qaysi biri to'g'ri — kodda emas, soliq qoidasida (fiskal chek har qator bo'yicha QQS talab qilishi mumkin). Muhimi — tanlovni hujjatlashtirish va hamma joyda bir xil qo'llash. Oxirgi qator: 1 071 429 tiyin = 10 714.29 so'm — big.js bilan mos.
12. Loyiha qadami: pul.ts moduli
To'lov darslari (shu darsdan to refund va moliyaviy hisobgacha) uchun kanon alohida paket yaratdi: tolov/. U vazifalar-api ga kirmaydi — 38-qismdagi bozor loyihasida (do'kon + Click/Payme) ishlatiladi. Paket: Node 24.21.0, TypeScript 6.0.3, Express 5.3.0, pg 8.23.1, stripe 23.0.0, Zod 4.6.7; npm run check — tsc + eslint (strictTypeChecked) + node --test.
Birinchi fayl — src/pul.ts. Darsdagi hamma funksiya shu yerda, TypeScript bilan. Telefon uchun uzun qatorlar va izohlarni bo'ldik, kod ma'nosi kanondagidek (bitta qatorli return ikki o'zgaruvchiga ajratildi):
// pul.ts — pul bilan ishlash: summa doim TIYINDA butun son (1 so'm =
// 100 tiyin). Float yo'q.
// number xavfsiz chegarasi 2^53 − 1 tiyin ≈ 90 trln so'm — bitta
// to'lov uchun yetarli; yig'indilar (hisobot, ledger) uchun bigint
// (Postgres bigint → pg string → BigInt).
export type Tiyin = number;
export class PulXato extends Error {
constructor(xabar: string) {
super(xabar);
this.name = "PulXato";
}
}
export function tiyinmi(x: unknown): x is Tiyin {
return typeof x === "number" && Number.isSafeInteger(x);
}
/**
* "12 500", "12500.5", "12 500,50" → tiyin. Faqat bitta kasr
* ajratgichi (`.` yoki `,`), minglik — bo'shliq/NBSP/`'`.
* Kasrdan keyin 0–2 raqam. Float orqali emas — satrdan to'g'ridan
* butun songa.
*/
export function somdanTiyin(kirish: string): Tiyin {
// \s JS'da NBSP (U+00A0) va tor NBSP (U+202F) ni ham o'z ichiga
// oladi (Intl chiqishi)
const t = kirish.trim().replace(/[\s']/g, "");
const m = /^(-?)(\d+)(?:[.,](\d{1,2}))?$/.exec(t);
if (!m) throw new PulXato(`noto'g'ri summa: "${kirish}"`);
const [, ishora = "", butun = "0", kasr = ""] = m;
const n = Number(butun) * 100 + Number(kasr.padEnd(2, "0"));
if (!Number.isSafeInteger(n))
throw new PulXato(`summa juda katta: "${kirish}"`);
return ishora === "-" ? -n : n;
}
/**
* Click SHOP API summasi so'mda, kasr bilan keladi ("1000",
* "1000.00", "1000.5") — tiyinga. Kasrda 2 dan ortiq raqam — xato
* (yaxlitlamaymiz).
*/
export const clickSummasi = (s: string): Tiyin =>
somdanTiyin(s.replace(",", "."));
/** Tiyin → "1000.00" (so'mda kutadigan API'lar uchun). */
export function tiyindanSom(t: Tiyin): string {
if (!tiyinmi(t))
throw new PulXato(`tiyin butun son emas: ${String(t)}`);
const m = Math.abs(t);
const som = String(Math.floor(m / 100));
const qoldiq = String(m % 100).padStart(2, "0");
return `${t < 0 ? "-" : ""}${som}.${qoldiq}`;
}
const formatlovchi = new Map<string, Intl.NumberFormat>();
/**
* Foydalanuvchiga ko'rsatish: `Intl.NumberFormat` + UZS. Faqat
* ko'rsatish uchun — hisoblashda ishlatilmaydi.
*/
export function korsat(
t: Tiyin,
til = "uz-Latn-UZ",
kasrsiz = false,
): string {
const kalit = `${til}|${String(kasrsiz)}`;
let f = formatlovchi.get(kalit);
if (!f) {
f = new Intl.NumberFormat(til, {
style: "currency",
currency: "UZS",
minimumFractionDigits: kasrsiz ? 0 : 2,
maximumFractionDigits: kasrsiz ? 0 : 2,
});
formatlovchi.set(kalit, f);
}
return f.format(t / 100);
}
/**
* Foiz (masalan komissiya) — "bank" yaxlitlashi (half-even); `foizBp`
* — basis point (1 % = 100 bp), float yo'q.
*/
export function foiz(t: Tiyin, foizBp: number): Tiyin {
const surat = BigInt(t) * BigInt(foizBp);
const maxraj = 10_000n;
const q = surat / maxraj;
const r = surat % maxraj;
const ikki = (r < 0n ? -r : r) * 2n;
let natija = q;
if (ikki > maxraj || (ikki === maxraj && q % 2n !== 0n))
natija += surat < 0n ? -1n : 1n;
return Number(natija);
}
/**
* Summani ulushlarga bo'lish — yo'qolgan tiyin yo'q: qoldiq birinchi
* ulushlarga bittadan (100 / 3 → 34, 33, 33).
*/
export function taqsimla(t: Tiyin, ulushlar: number[]): Tiyin[] {
if (
ulushlar.length === 0 ||
ulushlar.some((u) => !Number.isSafeInteger(u) || u < 0)
)
throw new PulXato("ulushlar manfiy emas butun son bo'lsin");
const jami = ulushlar.reduce((a, b) => a + b, 0);
if (jami === 0) throw new PulXato("ulushlar yig'indisi 0");
const natija = ulushlar.map((u) =>
Number((BigInt(t) * BigInt(u)) / BigInt(jami)),
);
let qoldiq = t - natija.reduce((a, b) => a + b, 0);
for (let i = 0; qoldiq !== 0; i = (i + 1) % natija.length) {
natija[i] = (natija[i] ?? 0) + Math.sign(qoldiq);
qoldiq -= Math.sign(qoldiq);
}
return natija;
}
/**
* Valyuta: USD sentdan so'm tiyiniga, kurs satr ("12 650.37" so'm / 1
* USD), half-even.
*/
export function sentdanTiyin(sent: number, kurs: string): Tiyin {
const k = somdanTiyin(kurs);
const surat = BigInt(sent) * BigInt(k);
const q = surat / 100n;
const r = surat % 100n;
return Number(
r * 2n > 100n || (r * 2n === 100n && q % 2n !== 0n)
? q + 1n
: q,
);
}TypeScript'dagi qo'shimchalar: tiyinmi — tur qo'riqchisi (type guard), PulXato — o'z xato klassi (testlar assert.throws(..., PulXato) bilan tekshiradi), korsat esa Intl.NumberFormat obyektlarini Map da keshlaydi — har chaqiruvda yangisini yaratish qimmat.
Testlar — tolov/test/pul.test.ts, beshta. Parcha (Intl testi):
test("kurs va ko'rsatish (Intl, ICU — Node 24)", () => {
// 19.99 × 12650.37 = 252 880.896 so'm → 25 288 090 tiyin
assert.equal(sentdanTiyin(1999, "12 650.37"), 25_288_090);
// ICU NBSP / tor NBSP ishlatadi
const bosh = (x: string) => x.replace(/\s/gu, " ");
const som = `so${String.fromCodePoint(0x2bb)}m`;
assert.equal(bosh(korsat(1_250_050)), `12 500,50 ${som}`);
assert.equal(
bosh(korsat(1_250_050, "ru-RU")), "12 500,50 UZS",
);
// Intl sukut yaxlitlashi halfExpand: 12 500,5 → 12 501
assert.equal(
bosh(korsat(1_250_050, "uz-Latn-UZ", true)), `12 501 ${som}`,
);
});Testni biz ishga tushirdik (kanon nusxasi, Node 24.21.0):
node --test test/pul.test.ts✔ somdanTiyin: formatlar va xatolar (2.52ms)
✔ float tuzog'i: 0.1 + 0.2 so'm — tiyinda aniq (0.2878ms)
✔ foiz: half-even, manfiy (0.1916ms)
✔ taqsimla: qoldiq yo'qolmaydi (1.1024ms)
✔ kurs va ko'rsatish (Intl, ICU — Node 24) (67.6695ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 265.1428Millisekundlar har ishga tushishda boshqacha. Kanonda butun tolov paketi npm run check dan o'tgan: tsc 0, eslint 0, testlar 33/33.
Commit xabari:
git commit -m "feat(tolov): pul.ts — tiyin, half-even foiz, taqsimlash, Intl"13. Real ishda
- Stripe, Payme, Uzum — hammasi summani eng kichik birlikda, butun son bilan qabul qiladi. Stripe hujjati "nol kasrli valyutalar" (JPY kabi) ro'yxatini alohida beradi: u yerda eng kichik birlik — valyutaning o'zi.
- Martin Fowler'ning Money naqshi — summa va valyutani bitta obyektda saqlash; dinero.js shu naqshning JavaScript'dagi amalga oshirilishi.
- Buxgalteriya sizdan "har tiyin qayerga ketdi?" deb so'raydi. Refund, reconciliation va moliyaviy hisob darsida ikki yoqlama yozuvni (double-entry) quramiz — u ham tiyinda.
- Intervyuda: "Nega
0.1 + 0.2 !== 0.3?", "Pulni qanday saqlaysiz?", "100 ni 3 ga qanday bo'lasiz?"
Xulosa
- Float pulga yaramaydi:
19.99 * 100 = 1998.9999…,1.005-bob.toFixed(2) = "1.00". - Summa — tiyinda butun son; satrdan float'siz o'qiladi, kasrda 2 dan ortiq raqam rad etiladi.
numberchegarasi — 90.1 trln so'm; yig'indilar va oraliq ko'paytmalar uchunBigInt; JSON'da satr.- Yaxlitlash qoidasini o'zingiz tanlaysiz: kanon — half-even;
Math.roundvaIntl— yarimni yuqoriga. - Bo'lishda qoldiq tarqatiladi: 100 / 3 → 34, 33, 33.
Intl.NumberFormat— faqat ko'rsatish;pgbigint/numericni satr qilib qaytaradi,moneyvafloat8ishlatilmaydi.
Keyingi dars: To'lov ekotizimi va umumiy oqim — merchant, provayder va bank kim, to'lov qanday oqadi, va tolov bazasining sxemasi.
Manbalar
- IEEE 754 va JavaScript sonlari: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
BigInt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigIntIntl.NumberFormat(roundingMode): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat- PostgreSQL
moneyturi vanumeric: https://www.postgresql.org/docs/18/datatype-money.html, https://www.postgresql.org/docs/18/datatype-numeric.html - node-postgres turlar: https://node-postgres.com/features/types
- Stripe, eng kichik valyuta birligi va nol kasrli valyutalar: https://docs.stripe.com/currencies (2026-10-28 da o'qildi)
- dinero.js: https://dinerojs.com, decimal.js: https://mikemcl.github.io/decimal.js/, big.js: https://mikemcl.github.io/big.js/
- Kanon tajribasi (IlmHamroh, 2026-10-27): decimal.js 10.6.0, big.js 7.0.1, dinero.js 2.0.2, pg 8.23.1, PostgreSQL 18.6, Node 24.21.0. Modul testlari va Postgres 17.6 so'rovi — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!