Jaka jest najlepsza metoda obsługi waluty / pieniędzy?


323

Pracuję nad bardzo podstawowym systemem koszyków.

Mam tabelę, itemsktóra ma kolumnę pricetypu integer.

Mam problem z wyświetleniem wartości ceny w moich widokach dla cen obejmujących zarówno euro, jak i centy. Czy brakuje mi czegoś oczywistego, jeśli chodzi o obsługę waluty w środowisku Rails?


jeśli ktoś używa sql, to DECIMAL(19, 4) jest popularnym wyborem sprawdź to również sprawdź tutaj Formaty waluty światowej, aby zdecydować, ile miejsc po przecinku użyć, nadzieja pomaga.
shaijut

Odpowiedzi:


495

Prawdopodobnie będziesz chciał użyć DECIMALtypu w swojej bazie danych. Podczas migracji wykonaj coś takiego:

# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, :precision => 8, :scale => 2

W Railsach :decimaltyp jest zwracany jako BigDecimal, co doskonale nadaje się do obliczania ceny.

Jeśli nalegasz na użycie liczb całkowitych, będziesz musiał ręcznie przekonwertować na BigDecimals wszędzie, co prawdopodobnie stanie się bólem.

Jak wskazał mcl, aby wydrukować cenę, użyj:

number_to_currency(price, :unit => "€")
#=> €1,234.01

13
Skorzystaj z pomocnika number_to_currency, więcej informacji na api.rubyonrails.org/classes/ActionView/Helpers/…
mlibby

48
W rzeczywistości znacznie bezpieczniej i łatwiej jest używać liczb całkowitych w połączeniu z act_as_dollars. Czy kiedykolwiek gryzło Cię porównanie zmiennoprzecinkowe? Jeśli nie, nie rób z tego pierwszego doświadczenia. :) Dzięki Act_as_dollars wstawiasz rzeczy w formacie 12.34, jest ono przechowywane jako 1234 i wychodzi jako 12.34.
Sarah Mei,

50
@ Sarah Mei: BigDecimals + format kolumny dziesiętnej dokładnie tego unika.
molf

114
Ważne jest, aby nie kopiować tej odpowiedzi na ślepo - precyzja 8, skala 2 daje maksymalną wartość 999,999.99 . Jeśli potrzebujesz liczby większej niż milion, zwiększ precyzję!
Jon Cairns,

22
Ważne jest również, aby nie używać na ślepo skali 2, jeśli posługujesz się różnymi walutami - niektóre waluty północnoafrykańskie i arabskie, takie jak rial omański lub dinar tunezyjski, mają skalę 3, więc dokładność 8 skala 3 jest bardziej odpowiednia .
Pokonaj Richartz

117

Oto dobre, proste podejście, które wykorzystuje composed_of(część ActiveRecord, używając wzorca ValueObject) i klejnot Money

Będziesz potrzebował

  • Gem Pieniądze (wersja 4.1.0)
  • Na przykład model Product
  • Na przykład integerkolumna w twoim modelu (i bazie danych):price

Zapisz to w swoim product.rbpliku:

class Product > ActiveRecord::Base

  composed_of :price,
              :class_name => 'Money',
              :mapping => %w(price cents),
              :converter => Proc.new { |value| Money.new(value) }
  # ...

Co otrzymasz:

  • Bez żadnych dodatkowych zmian wszystkie formularze będą wyświetlać dolary i centy, ale wewnętrzna reprezentacja to nadal tylko centy. Formularze zaakceptują wartości takie jak „12 034,95 $” i przekonwertują je dla Ciebie. Nie ma potrzeby dodawania dodatkowych modułów obsługi lub atrybutów do modelu ani pomocników w twoim widoku.
  • product.price = "$12.00" automatycznie konwertuje do klasy Money
  • product.price.to_s wyświetla dziesiętnie sformatowaną liczbę („1234,00”)
  • product.price.format wyświetla poprawnie sformatowany ciąg dla waluty
  • Jeśli musisz wysłać centy (do bramki płatności, która chce groszy), product.price.cents.to_s
  • Przeliczanie walut za darmo

14
Uwielbiam to podejście. Ale pamiętaj: upewnij się, że migracja dla „ceny” w tym przykładzie nie zezwala na wartości zerowe i domyślnie wynosi 0, abyś nie oszalał próbując dowiedzieć się, dlaczego to nie działa.
Cory

3
Odkryłem, że klejnot money_column (wyodrębniony z Shopify) jest bardzo prosty w użyciu ... łatwiejszy niż klejnot pieniężny, jeśli nie potrzebujesz przeliczania walut.
talyric

7
Dla wszystkich tych, którzy używają klejnotu Money, należy zauważyć, że zespół podstawowy Railsów dyskutuje nad wycofywaniem i usuwaniem „composite_of” ze środowiska. Podejrzewam, że klejnot zostanie zaktualizowany, aby sobie z tym poradzić, jeśli tak się stanie, ale jeśli patrzysz na Rails 4.0, powinieneś zdawać sobie sprawę z tej możliwości
Peer Allan

1
Odnośnie komentarza @ PeerAllan na temat usunięcia composed_of tutaj jest więcej szczegółów na ten temat, a także alternatywna implementacja.
HerbCSO,

3
Ponadto jest to naprawdę łatwe przy użyciu klejnotu pieniędzy za szyny .
fotanus

25

Powszechną praktyką w zakresie obsługi waluty jest stosowanie typu dziesiętnego. Oto prosty przykład z „Agile Web Development with Rails”

add_column :products, :price, :decimal, :precision => 8, :scale => 2 

Umożliwi to obsługę cen od -999,999,99 do 999,999,99.
Możesz także chcieć uwzględnić weryfikację w swoich produktach, takich jak

def validate 
  errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01 
end 

by sprawdzić zdrowie psychiczne.


1
To rozwiązanie umożliwia także korzystanie z sumy SQL i znajomych.
Larry K

4
Czy możesz zrobić: zatwierdza: cena,: obecność => prawda,: numeryczność => {: greater_than => 0}
Galaxy

9

Jeśli korzystasz z Postgres (a ponieważ jesteśmy w 2017 roku), możesz :moneyspróbować wypróbować typ kolumny.

add_column :products, :price, :money, default: 0

7

Użyj klejnotu z szynami pieniędzy . Ładnie radzi sobie z pieniędzmi i walutami w twoim modelu, a także ma grupę pomocników w sformatowaniu twoich cen.


Tak, zgadzam się z tym. Zasadniczo zajmuję się pieniędzmi, przechowując je w centach (liczbach całkowitych) i wykorzystując klejnot, taki jak pieniądze lub pieniądze (szyny pieniężne), do obsługi danych w pamięci. Obsługa liczb całkowitych zapobiega tym nieprzyjemnym błędom zaokrąglania. Np. 0,2 * 3 => 0,6000000000000001 To oczywiście działa tylko wtedy, gdy nie musisz obsługiwać ułamków centa.
Chad M

Jest to bardzo miłe, jeśli używasz szyn. Wrzuć i nie martw się problemami z kolumną dziesiętną. Jeśli użyjesz tego z widokiem, ta odpowiedź może być również pomocna: stackoverflow.com/questions/18898947/...
zacumowany

6

Tylko niewielka aktualizacja i spójność wszystkich odpowiedzi dla niektórych początkujących juniorów / początkujących w rozwoju RoR, które z pewnością pojawią się tutaj po kilka wyjaśnień.

Praca z pieniędzmi

Służy :decimaldo przechowywania pieniędzy w DB, jak sugerował @molf (i co moja firma stosuje jako złoty standard podczas pracy z pieniędzmi).

# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, precision: 8, scale: 2

Kilka punktów:

  • :decimalbędzie używany jako BigDecimalrozwiązanie wielu problemów.

  • precisioni scalenależy je dostosować, w zależności od tego, co reprezentujesz

    • Jeśli pracujesz z otrzymywaniem i wysyłaniem płatności, precision: 8a scale: 2otrzymasz 999,999.99najwyższą kwotę, co jest w porządku w 90% przypadków.

    • Jeśli chcesz przedstawić wartość nieruchomości lub rzadkiego samochodu, powinieneś użyć wyższej precision.

    • Jeśli pracujesz ze współrzędnymi (długość i szerokość geograficzną), na pewno będziesz potrzebować wyższej scale.

Jak wygenerować migrację

Aby wygenerować migrację z powyższą zawartością, uruchom terminal:

bin/rails g migration AddPriceToItems price:decimal{8-2}

lub

bin/rails g migration AddPriceToItems 'price:decimal{5,2}'

jak wyjaśniono w tym poście na blogu .

Formatowanie walut

POCZUJ dodatkowe biblioteki i pożegnaj się z wbudowanymi pomocnikami. Użyj number_to_currencyjako @molf i @facundofarias.

Grać z number_to_currencypomocnika w konsoli Rails, wysłać wezwanie do ActiveSupport„s NumberHelperklasy w celu uzyskania dostępu do pomocnika.

Na przykład:

ActiveSupport::NumberHelper.number_to_currency(2_500_000.61, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")

daje następujący wynik

2500000,61

Sprawdź inne optionsod number_to_currency pomocnika.

Gdzie to położyć

Możesz umieścić go w pomocniku aplikacji i używać go w widokach dla dowolnej kwoty.

module ApplicationHelper    
  def format_currency(amount)
    number_to_currency(amount, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

Możesz też umieścić go w Itemmodelu jako metodę instancji i nazwać go tam, gdzie musisz sformatować cenę (w widokach lub pomocnikach).

class Item < ActiveRecord::Base
  def format_price
    number_to_currency(price, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

I przykład, w jaki sposób korzystam z number_to_currencywewnątrz kontrolera (zwróć uwagę na negative_formatopcję, służącą do przedstawienia refundacji)

def refund_information
  amount_formatted = 
    ActionController::Base.helpers.number_to_currency(@refund.amount, negative_format: '(%u%n)')
  {
    # ...
    amount_formatted: amount_formatted,
    # ...
  }
end

5

Korzystając z atrybutów wirtualnych (Link do poprawionej (płatnej) wersji Railscast) , możesz przechowywać swoje price_in_cents w kolumnie liczb całkowitych i dodać wirtualny atrybut price_in_dollars w modelu produktu jako getter i setter.

# Add a price_in_cents integer column
$ rails g migration add_price_in_cents_to_products price_in_cents:integer

# Use virtual attributes in your Product model
# app/models/product.rb

def price_in_dollars
  price_in_cents.to_d/100 if price_in_cents
end

def price_in_dollars=(dollars)
  self.price_in_cents = dollars.to_d*100 if dollars.present?
end

Źródło: RailsCasts # 016: Wirtualne atrybuty : Wirtualne atrybuty to czysty sposób na dodawanie pól formularzy, które nie są mapowane bezpośrednio do bazy danych. Tutaj pokazuję, jak obsługiwać walidacje, skojarzenia i wiele innych.


1
pozostawia to 200,0 jedną cyfrę
ajbraus,


2

Jeśli ktoś używa Sequel, migracja wyglądałaby mniej więcej tak:

add_column :products, :price, "decimal(8,2)"

jakoś Sequel ignoruje: precyzję i: skalę

(Sequel Version: sequel (3.39.0, 3.38.0))


2

Wszystkie moje podstawowe interfejsy API używały centów do reprezentowania pieniędzy i nie chciałem tego zmieniać. Nie pracowałem też z dużymi pieniędzmi. Po prostu umieściłem to w metodzie pomocniczej:

sprintf("%03d", amount).insert(-3, ".")

To konwertuje liczbę całkowitą na ciąg z co najmniej trzema cyframi (w razie potrzeby dodając zera na początku), a następnie wstawia przecinek przed ostatnimi dwiema cyframi, nigdy nie używając Float. Stamtąd możesz dodać symbole walut odpowiednie dla danego przypadku użycia.

Jest to zdecydowanie szybkie i brudne, ale czasem jest w porządku!


Nie mogę uwierzyć, że nikt cię nie ocenił. To była jedyna rzecz, która działała, aby mój obiekt Money ładnie przybierał formę taką, że API może go przyjąć. (Dziesiętny)
Code-MonKy

2

Używam tego w ten sposób:

number_to_currency(amount, unit: '€', precision: 2, format: "%u %n")

Oczywiście symbol waluty, precyzja, format itd. Zależą od każdej waluty.


1

Możesz przekazać niektóre opcje do number_to_currency(standardowego pomocnika widoku Rails 4):

number_to_currency(12.0, :precision => 2)
# => "$12.00"

Opublikowane przez Dylan Markow


0

Prosty kod dla Ruby & Rails

<%= number_to_currency(1234567890.50) %>

OUT PUT => $1,234,567,890.50
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.