Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tính toán tiền tệ chính xác trong Ruby: Tránh bẫy Float

Tiền tệ, bất kể đơn vị tiền tệ nào, trông giống như số float. Nhưng dùng float cho tiền tệ là sai lầm.

Số float (do đó object float) theo định nghĩa là số thực không chính xác sử dụng biểu diễn float độ chính xác kép đặc trưng của kiến trúc gốc. Số không chính xác làm kế toán viên không vui.

Trong bài viết này, bạn sẽ được hướng dẫn qua một số ví dụ nhanh giúp nắm bắt các lựa chọn xử lý dữ liệu tiền tệ trong Ruby và Rails.

Float là gì?

Như đã nói, object float có phép tính khác. Đây là lý do chính chúng là số không chính xác, đặc biệt vì Ruby (như hầu hết ngôn ngữ) dùng số chữ số nhị phân cố định để biểu diễn float. Nói cách khác, Ruby chuyển đổi số float từ thập phân sang nhị phân và ngược lại.

Khi đi sâu vào biểu diễn nhị phân của số thực (số thập phân), một số không thể biểu diễn chính xác, nên hệ thống chỉ còn cách làm tròn.

Nếu suy nghĩ trước và xem xét cấu trúc toán học phổ biến, như thập phân lặp, bạn có thể hiểu chúng không thể biểu diễn hoàn toàn trong số chữ số cố định vì số pi, ví dụ, là vô hạn. Float thường giữ đến 32 hoặc 64 bit độ chính xác, nghĩa là số sẽ bị cắt khi đạt giới hạn.

Hãy phân tích ví dụ kinh điển:

1200 * (14.0/100) 

Đây là cách trực tiếp tính phần trăm của một số. Mười bốn phần trăm của 1200 nên là 168; tuy nhiên, kết quả thực thi trong Ruby sẽ là

1200 * (14.0/100) => 168.00000000000003 

Tuy nhiên, nếu chỉ thêm 0.1% vào công thức, bạn nhận được kết quả khác:

1200 * (14.1/100) => 169.2 

Thay vào đó, bạn có thể round giá trị đến số thập phân mong muốn:

(my_calculation).round(2) 

Thực tế, không đảm bảo khi làm các tính toán phức tạp hơn, đặc biệt nếu so sánh các giá trị này.

Nếu bạn quan tâm hiểu khoa học thực sự đằng sau, tôi mạnh mẽ khuyên đọc phụ lục của Oracle: What Every Computer Scientist Should Know About Floating-Point Arithmetic. Nó giải thích chi tiết lý do bản chất không chính xác của số float.

BigDecimal đáng tin cậy

Xét đoạn code sau:

require "bigdecimal" BigDecimal("45.99") 

Code này dễ dàng biểu diễn logic thực của số tiền giỏ hàng eCommerce. Cuối cùng, giá trị thực bị thao tác luôn là 45.99 thay vì 45.9989 hoặc 45.99000009, ví dụ.

Đây chính là bản chất chính xác của BigDecimal. Đối với phép tính số học thông thường, float sẽ hoạt động giống nhau; tuy nhiên, nó không thể dự đoán, đây là nguy hiểm khi dùng nó.

Khi chạy với BigDecimal, cùng phép tính phần trăm ở phần trước cho kết quả

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

Đây chỉ là phiên bản ngắn để chạy nhanh trong console irb.

Ban đầu, khi in trực tiếp object BigDecimal, bạn sẽ nhận được ký pháp khoa học, không phải điều chúng ta muốn. Phương thức to_s nhận tham số cho cài đặt format và hiển thị giá trị float tương đương của BigDecimal. Để chi tiết hơn, tham khảo Ruby docs.

Nếu cần giới hạn chữ số thập phân, nó có phương thức truncate làm việc:

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

Dự án RubyMoney

RubyMoney được tạo ra sau khi suy nghĩ về những vấn đề này. Đây là cộng đồng open-source các nhà phát triển Ruby nhằm giúp đời lập trình viên bằng cách cung cấp thư viện tuyệt vời thao tác dữ liệu tiền tệ trong hệ sinh thái Ruby.

Dự án bao gồm bảy thư viện, ba trong số đó nổi bật:

  • Money: Thư viện Ruby xử lý tiền tệ và chuyển đổi tiền tệ. Cung cấp nhiều tùy chọn hướng đối tượng xử lý tiền tệ mạnh mẽ, hiện đại, bất kể là cho web hay không.
  • Money-rails: Tích hợp RubyMoney cho Ruby on Rails, kết hợp toàn bộ sức mạnh thư viện money với sự linh hoạt của Rails.
  • Monetize: Thư viện chuyển đổi các object khác thành object money. Hoạt động như thư viện phụ cho ứng dụng xử lý nhiều parsing String, ví dụ.

Dự án còn có bốn thư viện thú vị khác:

  • EU_central_bank: Thư viện tính tỷ giá hối đoái dùng tỷ giá công bố từ Ngân hàng Trung ương Châu Âu.
  • Google_currency: Thư viện chuyển đổi tiền tệ tham chiếu tỷ giá Google Currency.
  • Money-collection: Thư viện phụ tính toán chính xác tổng/min/max của object money.
  • Money-heuristics: Module phân tích heuristic đầu vào string cho gem money.

Gem "Money"

Hãy bắt đầu với gem nổi tiếng nhất: money. Các tính năng chính:

  • Class money giữ thông tin tiền tệ quan trọng: giá trị, đơn vị tiền tệ, dấu thập phân.
  • Class Money::Currency gói thông tin về đơn vị tiền tệ dùng bởi lập trình viên.
  • Mặc định dùng integer thay vì float để tránh lỗi nêu trên.
  • Khả năng chuyển đổi tiền tệ từ đơn vị này sang đơn vị khác, rất hay.

Ngoài ra, cũng có sự linh hoạt cao từ cấu trúc hướng đối tượng nhất quán thao tác dữ liệu tiền tệ, như bất kỳ model nào trong dự án.

Cách dùng đơn giản, chỉ cài gem:

gem install money 

Ví dụ nhanh với số tiền cố định:

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

Như bạn thấy, tiền được biểu diễn dựa trên cent. 1200 cent tương đương 12 đô la.

Giống như BigDecimal, bạn cũng có thể chơi với phép toán cơ bản trên object này. Ví dụ:

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

Thú vị, phải không? Đó là bản chất của object chúng ta nói. Khi code, thực sự cảm thấy như thao tác giá trị tiền tệ thay vì số vô nghĩa.

Chuyển đổi tiền tệ

Nếu bạn có hệ thống tỷ giá riêng, bạn có thể chuyển đổi qua object exchange bank. Xét:

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

Khi cần chuyển đổi giá trị giữa chúng, chạy code:

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

Tương tự áp dụng cho mọi phép toán và so sánh bạn muốn.

Hãy tham khảo docs để thêm thuộc tính tiền tệ, như iso_code (trả về mã 3 chữ số quốc tế của tiền tệ) và decimal_mark (ký tự giữa giá trị và phần thập phân), v.v.

À,差点 quên; sau khi cài money gem, bạn có thể truy cập phương thức BigDecimal tên to_money tự động chuyển đổi cho bạn.

Gem "monetize"

Quan trọng hiểu vai trò mỗi thư viện trong dự án RubyMoney. Khi cần chuyển object Ruby khác (ví dụ String) thành Money, monetize là thứ bạn cần.

Đầu tiên, đảm bảo cài gem dependency hoặc thêm vào Gemfile:

gem install monetize 

Rõ ràng, money cũng cần cài.

Phương thức parse cũng rất hữu ích khi nhận dữ liệu tiền tệ định dạng khác; ví dụ:

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

Mặc dù kịch bản dùng phương thức parse này hạn chế, nó rất hữu ích khi nhận giá trị kèm mã tiền tệ từ HTTP request. Trên web, mọi thứ là text, nên chuyển từ string sang money rất hữu ích.

Tuy nhiên, cẩn thận với cách hệ thống thao tác giá trị và xem có thể bị hack không. Hệ thống tài chính luôn có nhiều lớp bảo mật đảm bảo giá trị nhận được là giá trị thực của giao dịch.

Gem "monetize-rails"

Đây là thư viện xử lý thao tác tiền tệ tương tự, nhưng trong app Rails.

Tại sao cần thư viện thứ hai chỉ để chạy cùng Rails? Bạn chắc chắn có thể dùng gem money đơn lẻ trong dự án Rails cho phép toán thường. Tuy nhiên, nó không hoạt động đúng khi cấu trúc Rails cần giao tiếp với tính năng của money.

Xét ví dụ:

class Cart < ActiveRecord::Base monetize :amount_cents end 

Đây là object model Rails thực sự, chức năng. Bạn dùng cùng database (kể cả alias khi muốn tên attribute khác), Mongoid, REST web service, v.v.

Tất cả tính năng chúng ta đã biết cũng áp dụng cho gem này. Thường chỉ cần cài đặt thêm để chạy, đặt vào file config/initializers/money.rb:

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

Điều này đặt tiền tệ mặc định theo cung cấp. Tuy nhiên, trong development, bạn có thể cần chuyển đổi tiền tệ hoặc xử lý nhiều tiền tệ qua các model.

Nếu vậy, money-rails cho phép cấu hình tiền tệ ở cấp model:

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 

Lưu ý khi mọi thứ setup xong, thật dễ dùng kiểu tiền tệ cùng dự án.

Kết luận

Trong bài viết này, chúng ta đã khám phá các lựa chọn xử lý giá trị tiền tệ trong hệ sinh thái Ruby và Rails. Một số điểm quan trọng tóm lại:

  • Nếu xử lý tính toán số float, đặc biệt đại diện tiền tệ, hãy dùng BigDecimal hoặc instance Money.
  • Cố gắng chỉ dùng một hệ thống để tránh inconsistency thêm trong development.
  • Thư viện money là lõi của hệ RubyMoney, rất mạnh mẽ và trực tiếp. Money-rails là phiên bản tương đương cho Rails, và monetize cần thiết khi parse từ bất kỳ giá trị nào sang object Money.
  • Tránh dùng Float. Dù app không cần tính toán ngay, khả năng cao dev không cẩn thận sẽ làm sau này. Bạn có thể không ở đó để ngăn chặn.

Nhớ rằng, docs chính thức luôn phải đọc. BigDecimal đầy giải thích hay và ví dụ dùng, và RubyMoney gem projects cũng vậy.