การคำนวณทางการเงินที่แม่นยำใน JavaScript Gotchas คืออะไร?


125

เพื่อความสนใจในการสร้างรหัสข้ามแพลตฟอร์มฉันต้องการพัฒนาแอปพลิเคชันทางการเงินแบบง่ายๆใน JavaScript การคำนวณที่จำเป็นเกี่ยวข้องกับดอกเบี้ยทบต้นและทศนิยมที่ค่อนข้างยาว ฉันต้องการทราบข้อผิดพลาดที่ควรหลีกเลี่ยงเมื่อใช้ JavaScript ในการคำนวณประเภทนี้ถ้าเป็นไปได้!

คำตอบ:


107

คุณควรปรับขนาดทศนิยมของคุณเป็น 100 และแทนค่าเงินทั้งหมดเป็นเซนต์ทั้งหมด นี้คือการหลีกเลี่ยงปัญหาที่เกิดขึ้นกับตรรกะจุดลอยตัวและการคำนวณ ไม่มีประเภทข้อมูลทศนิยมใน JavaScript ประเภทข้อมูลที่เป็นตัวเลขเท่านั้นคือทศนิยม ดังนั้นโดยทั่วไปจึงแนะนำให้จัดการเงินเป็น2550เซ็นต์แทน25.50ดอลลาร์

พิจารณาว่าใน JavaScript:

var result = 1.0 + 2.0;     // (result === 3.0) returns true

แต่:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

การแสดงออก0.1 + 0.2 === 0.3ผลตอบแทนfalseแต่โชคดีจำนวนเต็มคณิตศาสตร์ในจุดลอยตัวเป็นที่แน่นอนดังนั้นข้อผิดพลาดการแสดงทศนิยมสามารถหลีกเลี่ยงได้โดยการปรับ1

โปรดทราบว่าในขณะที่ชุดของจำนวนจริงไม่มีที่สิ้นสุด แต่จะมีเพียงจำนวน จำกัด เท่านั้น (18,437,736,874,454,810,627 ที่แน่นอน) สามารถแสดงด้วยรูปแบบทศนิยมของ JavaScript ดังนั้นการเป็นตัวแทนของตัวเลขอื่น ๆ จะได้รับการประมาณของจำนวนจริง2


1ดักลาส Crockford: JavaScript: ดีอะไหล่ : ภาคผนวก A - อะไหล่ที่น่ากลัว (หน้า 105)
2เดวิดฟลานาแกน: JavaScript: แตกหักคู่มือฉบับที่สี่ : 3.1.3 Floating-Point ตัวอักษร (หน้า 31)


3
และเพื่อเป็นการเตือนความจำให้คำนวณรอบเป็นร้อยละเสมอและทำด้วยวิธีที่เป็นประโยชน์น้อยที่สุดสำหรับผู้บริโภค IE หากคุณกำลังคำนวณภาษีให้ปัดเศษ หากคุณกำลังคำนวณดอกเบี้ยที่ได้รับให้ตัดทอน
Josh

6
@Cirrostratus: คุณอาจต้องการตรวจสอบstackoverflow.com/questions/744099 หากคุณใช้วิธีการปรับมาตราส่วนโดยทั่วไปคุณจะต้องปรับขนาดค่าของคุณด้วยจำนวนหลักทศนิยมที่คุณต้องการรักษาความแม่นยำ หากคุณต้องการทศนิยม 2 ตำแหน่งให้ปรับขนาดด้วย 100 หากคุณต้องการ 4 ให้ปรับขนาด 10,000
Daniel Vassallo

2
... เกี่ยวกับค่า 3000.57 ใช่ถ้าคุณเก็บค่านั้นไว้ในตัวแปร JavaScript และคุณตั้งใจจะคำนวณเลขคณิตคุณอาจต้องการจัดเก็บค่านี้เป็น 300057 (จำนวนเซ็นต์) เพราะ3000.57 + 0.11 === 3000.68ผลตอบแทนfalse.
Daniel Vassallo

7
การนับเพนนีแทนดอลลาร์จะไม่ช่วย เมื่อนับเพนนีคุณจะไม่สามารถบวก 1 เป็นจำนวนเต็มได้โดยประมาณ 10 ^ 16 เมื่อนับดอลลาร์คุณจะสูญเสียความสามารถในการเพิ่ม. 01 เป็นตัวเลขที่ 10 ^ 14 มันเหมือนกันทั้งสองวิธี
slashingweapon

1
ฉันเพิ่งสร้างโมดูล npm และ Bowerซึ่งหวังว่าจะช่วยงานนี้ได้!
Pensierinmusica

18

การปรับขนาดทุกค่าด้วย 100 เป็นวิธีแก้ปัญหา การทำด้วยมืออาจไม่มีประโยชน์เนื่องจากคุณสามารถหาไลบรารีที่ทำเพื่อคุณได้ ฉันขอแนะนำ moneysafe ซึ่งมี API ที่ใช้งานได้เหมาะสำหรับแอปพลิเคชัน ES6:

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

ทำงานได้ทั้งใน Node.js และเบราว์เซอร์


2
upvoted จุด "สเกลคูณ 100" ครอบคลุมอยู่ในคำตอบที่ยอมรับแล้วอย่างไรก็ตามคุณควรเพิ่มตัวเลือกแพ็กเกจซอฟต์แวร์ที่มีไวยากรณ์ JavaScript ที่ทันสมัย FWIW in$, $ ชื่อค่ามีความคลุมเครือสำหรับผู้ที่ไม่เคยใช้แพ็กเกจมาก่อน ฉันรู้ว่ามันเป็นทางเลือกของเอริคที่จะตั้งชื่อสิ่งต่างๆในแบบนั้น แต่ฉันก็ยังรู้สึกว่ามันผิดพลาดมากพอที่ฉันอาจจะเปลี่ยนชื่อมันในคำสั่งนำเข้า / ทำลายโครงสร้าง
james_womack

4
การสเกลด้วย 100 จะช่วยได้จนกว่าคุณจะเริ่มอยากทำอะไรบางอย่างเช่นคำนวณเปอร์เซ็นต์ (ทำการหารเป็นหลัก)
Pointy

2
ฉันหวังว่าฉันจะเพิ่มคะแนนความคิดเห็นหลาย ๆ ครั้ง การสเกลด้วย 100 นั้นไม่เพียงพอ ประเภทข้อมูลตัวเลขเดียวใน JavaScript ยังคงเป็นประเภทข้อมูลทศนิยมและคุณจะยังคงพบข้อผิดพลาดในการปัดเศษที่สำคัญ
Craig

1
และหัวอื่นเพิ่มขึ้นจาก README Money$afe has not yet been tested in production at scale.นี้: เพียงแค่ชี้ให้ทุกคนสามารถพิจารณาได้ว่าเหมาะสมกับกรณีการใช้งานของตนหรือไม่
โนบิตะ

7

ไม่มีสิ่งที่เรียกว่าการคำนวณทางการเงินที่ "แม่นยำ" เนื่องจากมีเศษทศนิยมเพียงสองหลัก แต่นั่นเป็นปัญหาทั่วไปมากกว่า

ใน JavaScript คุณสามารถปรับขนาดทุกค่าด้วย 100 และใช้Math.round()ทุกครั้งที่เศษส่วนเกิดขึ้นได้

คุณสามารถใช้วัตถุเพื่อจัดเก็บตัวเลขและรวมการปัดเศษไว้ในvalueOf()วิธีการต้นแบบ แบบนี้:

sys = require('sys');

var Money = function(amount) {
        this.amount = amount;
    }
Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}

var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

ด้วยวิธีนี้ทุกครั้งที่คุณใช้ Money-object มันจะแสดงเป็นทศนิยมสองตำแหน่ง ค่าที่ไม่เกี่ยวข้องยังคงสามารถเข้าถึงได้ผ่านทางm.amount.

คุณสามารถสร้างอัลกอริธึมการปัดเศษของคุณเองได้Money.prototype.valueOf()หากต้องการ


ฉันชอบวิธีการเชิงวัตถุนี้การที่วัตถุ Money มีทั้งสองค่านั้นมีประโยชน์มาก เป็นฟังก์ชันประเภทเดียวกับที่ฉันต้องการสร้างในคลาส Objective-C ที่กำหนดเอง
james_womack

4
มันไม่แม่นยำพอที่จะปัดเศษ
Henry Tseng

3
ไม่ควร sys.puts (m + n); //80.76 จริงอ่าน sys.puts (m + n); //80.77? ฉันเชื่อว่าคุณลืมปัด. 5 ขึ้นไป
Dave L

2
วิธีการแบบนี้มีปัญหาที่ละเอียดอ่อนหลายประการที่สามารถครอบตัดได้ ตัวอย่างเช่นคุณยังไม่ได้ใช้วิธีการที่ปลอดภัยในการบวกการลบการคูณและอื่น ๆ ดังนั้นคุณมีแนวโน้มที่จะพบข้อผิดพลาดในการปัดเศษเมื่อรวมจำนวนเงิน
1800 ข้อมูล

2
ปัญหาในที่นี้คือเช่นMoney(0.1)หมายความว่า JavaScript lexer อ่านสตริง "0.1" จากแหล่งที่มาจากนั้นแปลงเป็นทศนิยมแบบไบนารีจากนั้นคุณได้ทำการปัดเศษโดยไม่ได้ตั้งใจแล้ว ปัญหาที่เกิดขึ้นเป็นเรื่องเกี่ยวกับการเป็นตัวแทน (ไบนารี VS ทศนิยม) ไม่ได้เกี่ยวกับความแม่นยำ
mgd


2

ปัญหาของคุณเกิดจากความไม่ถูกต้องในการคำนวณจุดลอยตัว หากคุณแค่ใช้การปัดเศษเพื่อแก้ปัญหานี้คุณจะมีข้อผิดพลาดมากขึ้นเมื่อคุณคูณและหาร

วิธีแก้ปัญหาอยู่ด้านล่างคำอธิบายดังนี้:

คุณจะต้องคิดเกี่ยวกับคณิตศาสตร์ที่อยู่เบื้องหลังสิ่งนี้เพื่อทำความเข้าใจ จำนวนจริงเช่น 1/3 ไม่สามารถแสดงในคณิตศาสตร์ด้วยค่าทศนิยมได้เนื่องจากไม่มีที่สิ้นสุด (เช่น - .333333333333333 ... ) ตัวเลขบางตัวในฐานสิบไม่สามารถแสดงเป็นเลขฐานสองได้อย่างถูกต้อง ตัวอย่างเช่น 0.1 ไม่สามารถแสดงในไบนารีได้อย่างถูกต้องโดยมีจำนวนตัวเลข จำกัด

ดูรายละเอียดเพิ่มเติมได้ที่นี่: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

ดูการใช้งานโซลูชัน: http://floating-point-gui.de/languages/javascript/


1

เนื่องจากลักษณะไบนารีของการเข้ารหัสตัวเลขทศนิยมบางตัวจึงไม่สามารถแสดงด้วยความแม่นยำที่สมบูรณ์แบบได้ ตัวอย่างเช่น

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

หากคุณต้องการใช้ javascript แท้คุณต้องคิดหาวิธีแก้ปัญหาสำหรับการคำนวณทุกครั้ง สำหรับโค้ดด้านบนเราสามารถแปลงทศนิยมเป็นจำนวนเต็มได้

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

การหลีกเลี่ยงปัญหาเกี่ยวกับคณิตศาสตร์ทศนิยมใน JavaScript

มีห้องสมุดเฉพาะสำหรับการคำนวณทางการเงินพร้อมเอกสารที่ยอดเยี่ยม Finance.js


ฉันชอบที่ Finance.js มีแอปตัวอย่างเช่นกัน
james_womack

0

น่าเสียดายที่คำตอบทั้งหมดได้เพิกเฉยต่อความจริงที่ว่าไม่ใช่ทุกสกุลเงินที่มีหน่วยย่อย 100 หน่วย (เช่นร้อยละคือหน่วยย่อยของดอลลาร์สหรัฐ (USD)) สกุลเงินเช่นดีนาร์อิรัก (IQD) มี 1,000 หน่วยย่อย: ดีนาร์อิรักมี 1,000 ไฟล์ เงินเยนของญี่ปุ่น (JPY) ไม่มีหน่วยย่อย ดังนั้น "คูณด้วย 100 เพื่อทำเลขคณิตจำนวนเต็ม" จึงไม่ใช่คำตอบที่ถูกต้องเสมอไป

นอกจากนี้สำหรับการคำนวณทางการเงินคุณต้องติดตามสกุลเงินด้วย คุณไม่สามารถเพิ่มดอลลาร์สหรัฐ (USD) ให้กับเงินรูปีของอินเดีย (INR) ได้ (โดยไม่ต้องแปลงหนึ่งเป็นอีกสกุลหนึ่งก่อน)

นอกจากนี้ยังมีข้อ จำกัด เกี่ยวกับจำนวนสูงสุดที่สามารถแสดงด้วยชนิดข้อมูลจำนวนเต็มของ JavaScript

ในการคำนวณทางการเงินคุณต้องจำไว้ด้วยว่าเงินมีความแม่นยำ จำกัด (โดยทั่วไปคือ 0-3 จุดทศนิยม) และการปัดเศษจะต้องทำในรูปแบบเฉพาะ (เช่นการปัดเศษ "ปกติ" เทียบกับการปัดเศษของนายธนาคาร) ประเภทของการปัดเศษที่จะดำเนินการอาจแตกต่างกันไปตามเขตอำนาจศาล / สกุลเงิน

วิธีจัดการเงินใน javascriptมีการอภิปรายประเด็นที่เกี่ยวข้องเป็นอย่างดี

ในการค้นหาของฉันฉันพบdinero.jsห้องสมุดที่อยู่หลายประเด็นที่ wrt การคำนวณทางการเงิน ยังไม่ได้ใช้มันในระบบการผลิตดังนั้นจึงไม่สามารถให้ความเห็นอย่างมีข้อมูลเกี่ยวกับมันได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.