วิธีที่ดีที่สุดในการจัดการสกุลเงิน / เงินคืออะไร?


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ซึ่งเหมาะสำหรับการคำนวณราคา

หากคุณยืนยันในการใช้จำนวนเต็มคุณจะต้องแปลงเป็นและจากBigDecimalทุกที่ด้วยตนเองซึ่งอาจจะกลายเป็นความเจ็บปวด

ตามที่ชี้แจงโดย mcl เพื่อพิมพ์ราคาใช้:

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

13
ใช้ตัวช่วยnumber_to_currency
mlibby

48
จริงๆแล้วมันปลอดภัยกว่าและง่ายกว่ามากในการใช้จำนวนเต็มร่วมกับ actions_as_dollars คุณเคยถูกกัดโดยการเปรียบเทียบจุดลอยตัวหรือไม่? ถ้าไม่ใช่อย่าทำให้นี่เป็นประสบการณ์ครั้งแรกของคุณ :) ด้วย actions_as_dollars คุณใส่เนื้อหาในรูปแบบ 12.34 มันเก็บเป็น 1234 และออกมาเป็น 12.34
ซาร่าห์เหม่ย

50
@Sarah Mei: รูปแบบ BigDecimals + คอลัมน์ทศนิยมหลีกเลี่ยงได้อย่างแม่นยำ
molf

114
มันเป็นสิ่งสำคัญที่จะไม่เพียงคัดลอกคำตอบนี้สุ่มสี่สุ่มห้า - ความแม่นยำ 8 ขนาด 2ช่วยให้คุณมีมูลค่าสูงสุดของ999,999.99 หากคุณต้องการตัวเลขที่มากกว่าหนึ่งล้านแล้วเพิ่มความแม่นยำ!
Jon Cairns

22
สิ่งสำคัญคือไม่เพียงแค่ใช้สเกล 2 อย่างสุ่มสี่สุ่มห้าหากคุณกำลังจัดการสกุลเงินที่แตกต่าง - สกุลเงินแอฟริกาเหนือและอาหรับเช่นโอมานเรียลหรือดีนาร์ตูนิเซียมีระดับ 3 ดังนั้นความแม่นยำ 8 สเกล 3จึงเหมาะสมกว่า .
เอาชนะ Richartz

117

นี่คือวิธีการที่ง่ายและเรียบง่ายที่ใช้ประโยชน์composed_of(ส่วนหนึ่งของ ActiveRecord โดยใช้รูปแบบ ValueObject) และ Money gem

คุณจะต้องการ

  • The Money gem (เวอร์ชั่น 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
ฉันรักวิธีการนี้ แต่โปรดทราบ: ตรวจสอบให้แน่ใจว่าการโยกย้ายของคุณเป็น 'ราคา' ในตัวอย่างนี้ไม่อนุญาตให้มีค่า Null และค่าเริ่มต้นเป็น 0 เพื่อมิให้คุณเสียสติลองคิดดูว่าทำไมสิ่งนี้ถึงใช้งานไม่ได้
คอรี

3
ฉันพบว่าmoney_column gem (สกัดจาก Shopify) นั้นตรงไปข้างหน้าเพื่อใช้ ... ง่ายกว่าอัญมณีเงินหากคุณไม่ต้องการแปลงสกุลเงิน
talyric

7
ควรสังเกตสำหรับผู้ที่ใช้งาน Money gem ว่าทีมหลักของ Rails กำลังพูดถึงการเลิกใช้งานและลบ "assemb_of" ออกจากกรอบ ฉันสงสัยว่าอัญมณีจะได้รับการอัปเดตเพื่อจัดการสิ่งนี้หากมันเกิดขึ้น แต่หากคุณกำลังมองหา Rails 4.0 คุณควรตระหนักถึงความเป็นไปได้นี้
Peer Allan

1
เกี่ยวกับความคิดเห็น @ PeerAllan เกี่ยวกับการลบcomposed_of ที่นี่เป็นรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้รวมถึงการดำเนินการทางเลือก
HerbCSO

3
นอกจากนี้ยังเป็นเรื่องง่ายมากที่จะใช้อัญมณีเงินราง
fotanus

25

แนวทางปฏิบัติทั่วไปสำหรับการจัดการสกุลเงินคือการใช้ประเภททศนิยม นี่คือตัวอย่างง่ายๆจาก "Agile Web Development with 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 

เพื่อสติตรวจสอบค่าของคุณ


1
โซลูชันนี้ยังช่วยให้คุณใช้ผลรวมของ SQL และเพื่อน ๆ
Larry K

4
คุณสามารถทำได้: ตรวจสอบความถูกต้อง: ราคา,: การแสดงตน => จริง,: numericality => {: Greater_than => 0}
Galaxy

9

หากคุณกำลังใช้ Postgres (และเนื่องจากเราอยู่ในปี 2560 ตอนนี้) คุณอาจต้องการ:moneyลองพิมพ์คอลัมน์ของพวกเขา

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

7

ใช้เงินรางอัญมณี มันจัดการเงินและสกุลเงินในแบบจำลองของคุณได้เป็นอย่างดีและยังมีผู้ช่วยมากมายในการจัดรูปแบบราคาของคุณ


ใช่ฉันเห็นด้วยกับสิ่งนี้ โดยทั่วไปฉันจัดการเงินโดยจัดเก็บเป็นเซ็นต์ (จำนวนเต็ม) และใช้อัญมณีเช่นการกระทำตามเงินหรือเงิน (money-rails) เพื่อจัดการข้อมูลในหน่วยความจำ การจัดการเป็นจำนวนเต็มจะช่วยป้องกันข้อผิดพลาดในการปัดเศษที่น่ารังเกียจ เช่น 0.2 * 3 => 0.6000000000000001 แน่นอนว่าใช้งานได้เฉพาะในกรณีที่คุณไม่ต้องจัดการกับเศษส่วนของร้อย
Chad M

มันดีมากถ้าคุณใช้ราง วางในและไม่ต้องกังวลเกี่ยวกับปัญหากับคอลัมน์ทศนิยม หากคุณใช้สิ่งนี้ด้วยมุมมองคำตอบนี้อาจมีประโยชน์เช่นกัน: 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 คอนโซลส่งการเรียกActiveSupport's NumberHelperระดับเพื่อให้สามารถเข้าถึงผู้ช่วย

ตัวอย่างเช่น:

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_currencycontrroler ภายใน (สังเกตเห็น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 ในรูปแบบผลิตภัณฑ์ของคุณในรูปแบบ getter และ 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

แหล่งที่มา: RailsCasts # 016: แอตทริบิวต์เสมือน : แอตทริบิวต์เสมือนเป็นวิธีใหม่ในการเพิ่มเขตข้อมูลฟอร์มที่ไม่แมปโดยตรงกับฐานข้อมูล ที่นี่ฉันจะแสดงวิธีจัดการการตรวจสอบความสัมพันธ์และอื่น ๆ


1
ใบนี้เหลือ 200.0 หนึ่งหลัก
ajbraus


2

หากมีใครบางคนกำลังใช้ผลสืบเนื่องการโยกย้ายจะมีลักษณะดังนี้:

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

ผลสืบเนื่องไม่สนใจ: ความแม่นยำและ: สเกล

(เวอร์ชั่นภาคต่อ: ภาคต่อ (3.39.0, 3.38.0))


2

API พื้นฐานของฉันทุกคนใช้เซ็นต์เพื่อแทนเงินและฉันไม่ต้องการเปลี่ยนมัน ฉันไม่ได้ทำงานด้วยเงินจำนวนมาก ดังนั้นฉันแค่ใส่มันในวิธีการช่วยเหลือ:

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

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

มันแน่นอนรวดเร็วและสกปรก แต่บางครั้งว่าเพียงแค่ปรับ!


ไม่อยากเชื่อเลยว่าไม่มีใครโหวตคุณ นี่เป็นสิ่งเดียวที่ทำงานเพื่อให้ได้เงินของฉันในรูปแบบที่ API สามารถใช้ได้ (ทศนิยม)
รหัส - MonKy

2

ฉันใช้มันด้วยวิธีนี้:

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

แน่นอนว่าสัญลักษณ์สกุลเงินความแม่นยำรูปแบบและอื่น ๆ ขึ้นอยู่กับแต่ละสกุลเงิน


1

คุณสามารถส่งตัวเลือกบางอย่างไปที่number_to_currency(ผู้ช่วยมุมมอง Rails 4 มาตรฐาน):

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

โพสต์โดยDylan Markow


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