Каков наилучший метод обращения с валютой / деньгами?

323

Я работаю над очень простой системой корзины покупок.

У меня есть таблица, itemsкоторая имеет столбец priceтипа integer.

У меня проблемы с отображением значения цены в моих представлениях для цен, которые включают в себя как евро, так и центы. Я упускаю что-то очевидное в том, что касается обработки валюты в рамках Rails?

Барри Галлахер
источник
если кто - то использует SQL, то DECIMAL(19, 4) является популярным выбор проверить это также проверить здесь мировую валюту форматов , чтобы решить , сколько знаков после запятой для использования, надежда помогает.
Shaijut

Ответы:

495

Возможно, вы захотите использовать DECIMALтип в вашей базе данных. В своей миграции сделайте что-то вроде этого:

# 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

В Rails :decimalтип возвращается как BigDecimal, что отлично подходит для расчета цены.

Если вы настаиваете на использовании целых чисел, вам придется вручную конвертировать в и из BigDecimals везде, что, вероятно, станет просто болью.

Как указано в mcl, для печати цены используйте:

number_to_currency(price, :unit => "€")
#=> €1,234.01
Molf
источник
13
Используйте помощник number_to_currency, больше информации на api.rubyonrails.org/classes/ActionView/Helpers/…
mlibby
48
На самом деле, гораздо безопаснее и проще использовать целое число в сочетании с acts_as_dollars. Вас когда-нибудь укусило сравнение с плавающей точкой? Если нет, не делайте это своим первым опытом. :) С act_as_dollars вы помещаете материал в формате 12.34, он хранится как 1234 и выходит как 12.34.
Сара Мей
50
@Sarah Mei: формат BigDecimals + десятичный столбец позволяет избежать именно этого.
Мольф
114
Важно не просто слепо копировать этот ответ - точность 8, шкала 2 дает максимальное значение 999 999,99 . Если вам нужно число больше миллиона, увеличьте точность!
Джон Кернс
22
Также важно не просто слепо использовать шкалу 2, если вы работаете с разными валютами - некоторые североафриканские и арабские валюты, такие как оманский риал или тунисский динар, имеют шкалу 3, поэтому точность 8 шкалы 3 здесь более уместна. ,
Удар Ричарц
117

Вот хороший, простой подход, который использует composed_of (часть ActiveRecord, используя шаблон ValueObject) и гем Money

Тебе понадобиться

  • Деньги камень (версия 4.1.0)
  • Модель, например Product
  • Например, integerстолбец в вашей модели (и базе данных):price

Запишите это в свой product.rbфайл:

class Product > ActiveRecord::Base

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

Что вы получите:

  • Без каких-либо дополнительных изменений все ваши формы будут показывать доллары и центы, но внутреннее представление все еще только центы. Формы будут принимать значения, такие как «$ 12 034,95» и конвертировать их для вас. Нет необходимости добавлять дополнительные обработчики или атрибуты к вашей модели или помощникам в вашем представлении.
  • product.price = "$12.00" автоматически конвертируется в класс Money
  • product.price.to_s отображает десятичное отформатированное число («1234.00»)
  • product.price.format отображает правильно отформатированную строку для валюты
  • Если вам нужно отправить центы (на платежный шлюз, который хочет копейки), product.price.cents.to_s
  • Конвертация валюты бесплатно
Кен Майер
источник
14
Мне нравится этот подход. Но обратите внимание: убедитесь, что ваша миграция для 'price' в этом примере не допускает нулевые значения и по умолчанию равна 0, чтобы вы не сошли с ума, пытаясь выяснить, почему это не работает.
Кори
3
Я обнаружил, что гем money_column (извлеченный из Shopify) очень прост в использовании ... проще, чем гем денег, если вам не нужна конвертация валюты.
Талик
7
Следует отметить, что для всех тех, кто использует гем Money, основная команда Rails обсуждает устаревание и удаление «mixed_of» из фреймворка. Я подозреваю, что гем будет обновлен, чтобы справиться с этим, если это произойдет, но если вы смотрите на Rails 4.0, вы должны знать об этой возможности
Peer Allan
1
Относительно комментария @ PeerAllan об удалении composed_of здесь более подробно об этом, а также об альтернативной реализации.
HerbCSO
3
Кроме того , это действительно reasy используя рельсы-деньги драгоценный камень.
Фотанус
25

Обычной практикой для обработки валюты является использование десятичного типа. Вот простой пример из "Agile Web Development с Rails"

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

Это позволит вам обрабатывать цены от -999 999,99 до 999 999,99.
Вы также можете включить проверку в такие элементы, как

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

в здравом уме - проверь свои ценности.

alex.zherdev
источник
1
Это решение также позволяет использовать сумму SQL и друзей.
Ларри К
4
Не могли бы вы сделать: validates: price, :sence => true,: numericity => {: more_than => 0}
Galaxy
9

Если вы используете Postgres (а теперь мы находимся в 2017 году), вы можете :moneyпопробовать их тип столбца.

add_column :products, :price, :money, default: 0
Призрак Оз
источник
7

Используйте деньги-рельсы драгоценный камень . Он прекрасно обрабатывает деньги и валюты в вашей модели, а также имеет несколько помощников для форматирования ваших цен.

Troggy
источник
Да, я согласен с этим. Как правило, я обращаюсь с деньгами, храня их в виде центов (целое число) и использую драгоценный камень, например действует как деньги или деньги (денежные рельсы), для обработки данных в памяти. Обработка его целыми числами предотвращает эти неприятные ошибки округления. Например, 0,2 * 3 => 0,6000000000000001 Это, конечно, работает, только если вам не нужно обрабатывать доли цента.
Чад М
Это очень хорошо, если вы используете рельсы. Оставьте это и не беспокойтесь о проблемах с десятичным столбцом. Если вы используете это для представления, этот ответ также может быть полезен: stackoverflow.com/questions/18898947/…
пришвартовано
6

Просто небольшое обновление и сплоченность всех ответов для некоторых начинающих юниоров / новичков в развитии RoR, которые обязательно придут сюда для некоторых объяснений.

Работа с деньгами

Используйте :decimalдля хранения денег в БД, как предложил @molf (и что моя компания использует как золотой стандарт при работе с деньгами).

# 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

Несколько баллов:

  • :decimalбудет использоваться в BigDecimalкачестве решения многих проблем.

  • precisionи scaleдолжны быть скорректированы, в зависимости от того, что вы представляете

    • Если вы работаете с получением и отправкой платежей, precision: 8и scale: 2вы получаете 999,999.99максимальную сумму, что хорошо в 90% случаев.

    • Если вам нужно представить стоимость имущества или редкого автомобиля, вы должны использовать более высокую precision.

    • Если вы работаете с координатами (долгота и широта), вам, безусловно, понадобится больше scale.

Как создать миграцию

Чтобы сгенерировать миграцию с указанным выше содержимым, запустите в терминале:

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

или

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

как объяснено в этом блоге .

Форматирование валюты

ПОЦЕЛУЙТЕ лишние библиотеки до свидания и используйте встроенные помощники. использованиеnumber_to_currency как предложено @molf и @facundofarias.

Для того, чтобы играть с number_to_currencyпомощником в консоли Rails, послать вызов к ActiveSupportNumberHelper класса для доступа помощника.

Например:

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

дает следующий вывод

2500000,61

Проверьте другой optionsиз number_to_currency помощника.

Где его поставить

Вы можете поместить его в помощник приложения и использовать его внутри представлений на любую сумму.

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

Или вы можете поместить его в Itemмодель как метод экземпляра и вызывать его там, где вам нужно отформатировать цену (в представлениях или помощниках).

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

И пример того, как я использую number_to_currencyвнутренний контроллер (обратите внимание на negative_formatопцию, которая используется для представления возвратов)

def refund_information
  amount_formatted = 
    ActionController::Base.helpers.number_to_currency(@refund.amount, negative_format: '(%u%n)')
  {
    # ...
    amount_formatted: amount_formatted,
    # ...
  }
end
Златко Аломерович
источник
5

Используя виртуальные атрибуты (ссылка на пересмотренный (платный) Railscast), вы можете сохранить ваши price_in_cents в целочисленном столбце и добавить виртуальный атрибут price_in_dollars в вашу модель продукта в качестве метода получения и установки.

# 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

Источник: RailsCasts # 016: Виртуальные атрибуты : Виртуальные атрибуты - это чистый способ добавления полей формы, которые не отображаются непосредственно в базу данных. Здесь я покажу, как обрабатывать проверки, ассоциации и многое другое.

Томас Клемм
источник
1
это оставляет 200.0 одну цифру
ajbraus
2

Определенно целые числа .

И хотя технически существующий BigDecimal 1.5все равно даст вам чистый Float в Ruby.

спорный вопрос
источник
2

Если кто-то использует Sequel, миграция будет выглядеть примерно так:

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

как-то сиквел игнорирует: точность и масштаб

(Версия сиквела: продолжение (3.39.0, 3.38.0))

jethroo
источник
2

Все мои базовые API использовали центы для представления денег, и я не хотел это менять. Я не работал с большими суммами денег. Так что я просто поместил это во вспомогательный метод:

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

Это преобразует целое число в строку, содержащую не менее трех цифр (при необходимости добавляя первые нули), затем вставляет десятичную точку перед последними двумя цифрами, никогда не используя a Float. Оттуда вы можете добавить любые символы валюты, которые подходят для вашего варианта использования.

Это определенно быстро и грязно, но иногда это просто отлично!

Брент Роял-Гордон
источник
Не могу поверить, что никто не голосовал против тебя. Это было единственное, что сработало, чтобы привести мой объект Money в такую ​​форму, чтобы его мог принять API. (Десятичное число)
Code-MonKy
2

Я использую это таким образом:

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

Конечно, что символ валюты, точность, формат и т. Д. Зависит от каждой валюты.

facundofarias
источник
0

Простой код для Ruby & Rails

<%= number_to_currency(1234567890.50) %>

OUT PUT => $1,234,567,890.50
Динеш Vaitage
источник