Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การคำนวณสกุลเงินที่แม่นยำใน Ruby: หลีกเลี่ยง Float Pitfalls

เงิน ไม่ว่าจะเป็นสกุลเงินอะไร ดูเหมือน Floating-point Number แต่การใช้ Floats สำหรับ Currency เป็นความผิด

Float Numbers (ดังนั้น Float Objects) ตามนิยามเป็น Real Numbers ที่ไม่แม่นยำ (inexact) ใช้ Double-precision Floating-point Representation ของ Native Architecture ตัวเลขที่ไม่แม่นยำนี้ทำให้ Accountants (นักบัญชี) ไม่อยากใช้

ในบทความนี้ คุณจะได้รับแนวทางผ่านตัวอย่างเร็วๆ ที่ช่วยให้คุณจัดการตัวเลือกที่มีอยู่สำหรับข้อมูลเงินใน Ruby และ Rails

Float คืออะไร?

เหมือนที่กล่าว float Objects มี Arithmetic ที่ต่างออกไป นี่คือเหตุผลหลักที่พวกมันเป็น Inexact Numbers โดยเฉพาะเพราะ Ruby (เช่นภาษาส่วนใหญ่) ใช้ Fixed Number ของ Binary Digits แทน Floats กล่าวคือ Ruby แปลง Floating Numbers จาก Decimal ไป Binary และกลับมาอีกครั้ง

เมื่อคุณล้ำลึกเข้า Binary Representation ของ Real Numbers (Decimal Numbers) บางตัวไม่สามารถแทนได้อย่างแม่นยำ ทำให้ระบบต้อง Round Off เหลือทางเลือกเดียว

ถ้าคิดล่วงหน้าและพิจารณาโครงสร้างคณิตศาสตร์ทั่วไป เช่น ทศนิยมซ้ำ (Periodic Tithes) คุณอาจเข้าใจว่าพวกมันไม่สามารถแทนได้ทั้งหมดภายใน Fixed Number เนื่องจากตัวเลข Pi เป็น Infinite Floats ปกติเก็บได้ 32 หรือ 64 Bits Precision หมายถึงตัวเลขจะถูก Cut Off เมื่อถึง Limit

ลองวิเคราะห์ตัวอย่างคลาสสิก:

1200 * (14.0/100) 

นี่คือวิธีตรงไปตรงมาคำนวณ Percentage 14% ของ 1200 ควรเป็น 168 แต่ผลลัพธ์ใน Ruby จะเป็น

1200 * (14.0/100) => 168.00000000000003 

อย่างไรก็ตาม ถ้าเพิ่มแค่ 0.1% ในสูตร คุณจะได้ผลต่าง:

1200 * (14.1/100) => 169.2 

ทางเลือกคือ round ค่าไปยังค่าที่ใกล้ที่สุด กำหนด Decimal Places ที่ต้องการ:

(my_calculation).round(2) 

แต่ไม่รับประกันใน Calculations ซับซ้อน โดยเฉพาะถ้า Compare ค่าเหล่านี้

ถ้าสนใจวิทยาศาสตร์จริง แนะนำอ่าน Oracle's Appendix: What Every Computer Scientist Should Know About Floating-Point Arithmetic อธิบายรายละเอียด Why ของ Inaccurate Nature ของ Float Numbers

BigDecimal ที่เชื่อถือได้

พิจารณา Code Snippet นี้:

require "bigdecimal" BigDecimal("45.99") 

โค้ดนี้แทน Real Logic ของยอด Cart ใน E-commerce ได้ง่าย ตอนท้ายค่าจริงที่ถูก Manipulate จะเป็น 45.99 เสมอ แทนที่จะเป็น 45.9989 หรือ 45.99000009

นี่คือลักษณะแม่นยำของ BigDecimal สำหรับ Arithmetic Calculations ปกติ float ทำงานเหมือนกัน แต่เป็น Unpredictable ซึ่งเป็นอันตรายของการใช้ Float

เมื่อรันกับ BigDecimal การคำนวณ Percentage เดียวกันในส่วนก่อน ผลลัพธ์คือ

require "bigdecimal" (BigDecimal(1200) * (BigDecimal(14)/BigDecimal(100))).to_s("F") => 168.0 

นี่เป็น Short Version สำหรับรันเร็วใน irb Console

ตอน Print BigDecimal Object โดยตรง จะได้ Scientific Notation ซึ่งไม่ใช่สิ่งที่เราต้องการ to_s Method รับ Argument ตาม Formatting Settings และแสดงค่า Floating เทียบเท่าของ BigDecimal ดูรายละเอียดเพิ่มใน Ruby Docs

ถ้าต้องการกำหนด Limit Decimal Places มี Method truncate ทำได้:

(BigDecimal(1200) * (BigDecimal("14.12")/BigDecimal(100))).truncate(2).to_s("F") => 169.44 

RubyMoney Project

RubyMoney ถูกสร้างหลังจากคิดถึงปัญหาเหล่านี้ เป็น Open-source Community ของ Ruby Developers ที่มุ่งชีวิต Developer ให้ง่ายขึ้นโดยให้ Libraries ยอดเยี่ยมสำหรับจัดการ Money Data ใน Ruby Ecosystem

โปรเจกต์ประกอบด้วย 7 Libraries 3 ตัวโดดเด่น:

  • Money: Ruby Library สำหรับจัดการเงินและ Currency Conversion มี OO Options หลายอย่าง Handle Money ใน Apps ที่ Robust และ Modern ไม่ว่าจะเป็น Web หรือไม่
  • Money-rails: Integration ของ RubyMoney สำหรับ Ruby on Rails ผสมพลัง Library money กับ Rails Flexibility
  • Monetize: Library สำหรับ Convert Objects ต่างๆ ให้เป็น money Objects ทำงานเหมือน Auxiliary Library สำหรับ Apps ที่จัดการ String Parsing มาก

โปรเจกต์มี Libraries อื่นๆ 4 ตัวที่น่าสนใจ:

  • EU_central_bank: Library ช่วยคำนวณ Exchange Rates ด้วย Published Rates จาก European Central Bank
  • Google_currency: Library สนุกสำหรับ Currency Conversion ใช้ Google Currency Rates อ้างอิง
  • Money-collection: Auxiliary Library สำหรับคำนวณ Sum/Min/Max ของ money Objects อย่างแม่นยำ
  • Money-heuristics: Module สำหรับ Heuristic Analyses ของ String Input สำหรับ Money Gem

The "Money" Gem

เริ่มจากที่มีชื่อเสียงที่สุด: Money Gem ฟีเจอร์หลักๆ:

  • Class money เก็บข้อมูล Monetary ที่เกี่ยวข้อง เช่น Value, Currency, Decimal Marks
  • Class อีกตัว Money::Currency Wrap ข้อมูล Monetary Unit ที่ Developer ใช้
  • Default ใช้ Integers แทน Floating-point Numbers เพื่อหลีกเลี่ยง Errors ที่กล่าวมา
  • ความสามารถ Exchange Money จาก Currency หนึ่งไปอีก Currency (Super Cool)

นอกจากนี้ เรายังได้ Flexibility สูงจากโครงสร้าง OO ที่สม่ำเสมอ จัดการ Money Data เหมือน Model อื่นๆ ใน Projects

Usage ง่าย ติดตั้ง Gem:

gem install money 

ตัวอย่างเร็วเกี่ยวกับ Fixed Amount:

my_money = Money.new(1200, "USD") my_money.cents #=> 1200 my_money.currency #=> Currency.new("USD") 

เห็นได้ว่า Money แทนค่าบน Cent 1200 Cents = 12 Dollars

เหมือน BigDecimal คุณสามารถเล่น Math พื้นฐานกับ Objects นี้ได้ เช่น

cart_amount = Money.new(10000, "USD") #=> 100 USD discount = Money.new(1000, "USD") #=> 10 USD cart_amount - discount == Money.new(9000, "USD") #=> 90 USD 

น่าสนใจใช่ไหม? นี่คือ Nature ของ Objects ที่กล่าวมา ตอน Coding รู้สึกเหมือน Manipulate ค่า Monetary จริง ไม่ใช่ตัวเลข Ordinary ที่ไร้ความหมาย

Currency Conversions

ถ้าคุณมี Exchange Rate System ของตัวเอง คุณสามารถ Convert Currencies ผ่าน Exchange Bank Object พิจารณา:

Money.add_rate("USD", "BRL", 5.23995) Money.add_rate("BRL", "USD", 0.19111) 

เมื่อต้อง Exchange Values ระหว่างพวกมัน รันโค้ดนี้:

Money.us_dollar(100).exchange_to("BRL") #=> Money.new(523, "BRL") 

เหมือนกันสำหรับ Arithmetic และ Comparison Evaluations ที่คุณอยากทำ

อย่าลืมดู Docs สำหรับ Currency Attributes เพิ่ม เช่น iso_code (คืน International 3-digit Code) และ decimal_mark (Char ระหว่าง Value และ Fraction) และอื่นๆ

โอ้ ผมเกือบลืม: ติดตั้ง Money Gem แล้ว คุณสามารถเข้าถึง Method to_money ของ BigDecimal ที่ Convert ให้อัตโนมัติ

The "monetize" Gem

สำคัญที่จะเข้าใจ Role ของแต่ละ Library ใน RubyMoney Project เมื่อต้อง Convert Ruby Object อื่น (เช่น String) ให้เป็น Money แล้ว monetize คือสิ่งที่คุณหา

แรก ติดตั้ง Gem Dependency หรือเพิ่มใน Gemfile:

gem install monetize 

ชัดเจนว่า money ก็ต้องติดตั้งด้วย

Method parse มีประโยชน์มากเมื่อรับ Money Data ใน Format ต่างๆ เช่น

Money.parse("£100") == Money.new(100, "GBP") #=> true 

แม้ Scenarios ที่ใช้ Parsing Method นี้จะจำกัด แต่มีประโยชน์มากเมื่อรับ Value Formatted พร้อม Currency Code จาก HTTP Request บน Web ทุกอย่างเป็น Text การ Convert String เป็น money มีประโยชน์มาก

อย่างไรก็ตาม ระวังว่าระบบของคุณ Manipulate Values ยังไง และ Hack ได้ไหม Financial Systems มี Multiple Security Layers รับรองว่า Value ที่รับคือ Real Value ของ Transaction

The "monetize-rails" Gem

Library นี้จัดการ Money Manipulation Operations เดียวกัน แต่ใน Rails App

ทำไมต้อง Library ที่สองเพื่อทำงานกับ Rails? คุณแน่นอนใช้ money Gem ลงใน Rails Projects สำหรับ Ordinary Math Operations ได้ แต่จะไม่ทำงานถูกต้องเมื่อ Rails Structures ต้อง Communicate กับ Features ของ money

พิจารณาตัวอย่าง:

class Cart < ActiveRecord::Base monetize :amount_cents end 

นี่คือ Rails Model Object จริงที่ Functional ใช้กับ Databases ได้ (รวม Aliases เมื่ออยากชื่อ Attribute ต่างใน Model), Mongoid, REST Web Services ฯลฯ

ฟีเจอร์ทั้งหมดที่ผ่านมาก็ Apply ได้กับ Gem นี้ ปกติแค่ Additional Settings เพื่อทำให้รัน วางใน config/initializers/money.rb:

MoneyRails.configure do |config| # set the default currency config.default_currency = :usd end 

นี่ Set Default Currency เป็นที่คุณให้ แต่ระหว่าง Development คุณอาจต้อง Exchange Conversions หรือ Handle Multiple Currencies ทั่ว Models

ถ้าเป็นเช่นนั้น money-rails 允许 Model-level Currency Definition:

class Cart < ActiveRecord::Base # Use GPB as model level currency register_currency :eur monetize :amount_cents, as "amount" monetize :discount_cents, with_currency: :eur monetize :converted_value, with_currency: :usd end 

สังเกตว่า Setup เสร็จแล้ว ใช้ Money Types กับ Projects ได้ง่ายมาก

สรุป

ในโพสต์นี้ เราสำรวจตัวเลือกสำหรับจัดการ Money Values ใน Ruby และ Rails Ecosystems สรุปจุดสำคัญ:

  • ถ้าจัดการ Calculation ของ Float Numbers โดยเฉพาะถ้าเป็น Money Data ใช้ BigDecimal หรือ Money Instances
  • พยายาม Stick to One System เท่านั้น เพื่อหลีกเลี่ยง Inconsistencies เพิ่มเติมระหว่าง Development
  • money Library คือ Core ของ RubyMoney System Robust และ Straightforward Money-rails คือ Version เทียบเท่าสำหรับ Rails Apps และ monetize จำเป็นเมื่อต้อง Parse จาก Value ใดๆ เป็น Money Objects
  • หลีกเลี่ยง Float แม้ App ไม่ต้อง Calculate ตอนนี้ โอกาสที่ Dev ไม่ฉลาดจะทำในอนาคตสูง คุณอาจไม่อยู่เพื่อหยุดมัน

จำไว้ Official Docs ควรเป็น Must-read BigDecimal เต็มไปด้วย Explanations และ Examples และ RubyMoney Gem Projects เหมือนกัน