użytkownik może ją wpisywać z przecinkiem lub kropką, max. 5 miejsc po przecinku,
konwersja z “ludzkiego” formatu na wewnętrzny bazy idzie wg wzoru:
input * 100000000 (oczywiście trzeba najpierw zamienić przecinek na kropkę).
Próbuję coś takiego, ale bez sukcesu:
class Bid < ActiveRecord::Base
validates :user_id, presence: true
validates :bet_id, presence: true
validates :amount, presence: true, numericality: true,
format: { :with => /\A\d{1,3}([\.,]\d{0,5})?\z/i }
before_save :btc_to_stc
private
def btc_to_stc
if new_record?
self.amount = 100000000 * amount_before_type_cast.to_s.gsub(',', '.').to_f
end
end
end
Ale - po wpisaniu prawidłowej liczby w formularzu, wartość zapisywana jest prawidłowo (“0.123” => 12300000), natomiast takie testy nie przechodzą:
describe "should be invalid with not valid amount" do
before { @bid.amount = "aassdd" }
it { should_not be_valid }
end
describe "amount" do
describe "should be converted" do
before do
@bid.amount = "0.1"
@bid.save
end
its(:amount) { should eq 10_000_000 }
end
describe "should be converted" do
before do
@bid.amount = "0,2"
@bid.save
end
its(:amount) { should eq 20_000_000 }
end
Może to złe podejście jest. Może trzeba:
zapisywać i obrabiać jako float/decimal (ale integer jest bardziej odpowiedni do operacji “walutowych”),
przerzucić całą konwersję do kontrolera,
użyć ValueObject (ale w tym przypadku niepotrzebna komplikacja, wystarczą operacje na liczbach a do wyświetlenia podzielić, zaokrąglić i po sprawie).
OK, pokombinowałem i doszedłem do działającego rozwiązania:
class Bid < ActiveRecord::Base
validates :amount, presence: true, numericality: true
before_validation :amount_in_stc
private
def amount_in_stc
def numeric(n)
s = n.to_s.gsub(',', '.')
s.to_i.to_s == s || s.to_f.to_s == s
end
if !amount.nil?
if numeric(amount_before_type_cast)
self.amount = (100000000 * amount_before_type_cast.to_s.gsub(',', '.').to_f).to_i
end
end
end
end
Testy:
it "should be invalid" do
inputs = %w[0,,2 asdf 3..3 3a]
inputs.each do |invalid_input|
@bid.amount = invalid_input
expect(@bid).not_to be_valid
end
end
it "should be valid" do
inputs = %w[0,2 3.3 3]
inputs.each do |valid_input|
@bid.amount = valid_input
expect(@bid).to be_valid
end
end
Więc to działa. Olałem na razie sprawdzenie dokładności do 5 miejsc, bo to można łatwo zrobić (regexem albo zaokrąglając do 5 mpp i porównując z oryginałem).
Jedyna mała niedogodność to, że przy błędzie do formularza wstawiana jest wartość po konwersji, więc trzeba w kontrolerze przypisać wartość oryginalnie wpisaną:
def create
@bet = Bet.find(params[:bet_id])
@bid = @bet.bids.build(bid_params)
if @bid.save
...
else
@bid.amount = bid_params[:amount]
render :template => 'bets/show'
end
end
robiłem coś podobnego w PHP i były problemy z float (np. z intval(floor(15)) wychodziło 14, bo tak naprawdę było przechowywane jako 14.9999999999999… ),
szybkość (ale to bardzo mała waga, dopóki nie mówimy o naprawdę dużej ilości obliczeń) i prostszy kod.
Możemy zrobić ankietę, kto by co wybrał (integer, float, decimal) do takiego zastosowania. Ja robię ćwiczeniowo, więc to nie musi być ani w 100% zgodne ze standardami przemysłowymi ani super optymalne
Chociaż dobrze by było wiedzieć, co jest standardem i co jest optymalne.
Możemy przenieść dyskusję do innego działu, bo tutaj pomoc już została udzielona. Jeszcze faktycznie pokombinuję z getter/setter jak podpowiadacie.
No i znów pokombinowałem (sport to zdrowie :P) i zrobiłem getter/setter, jak to zaproponowali Tomash i konole:
def amount_in_stc=(value)
s = value.to_s.gsub(',', '.')
if s.to_i.to_s == s || s.to_f.to_s == s || s.to_f.to_s == "0" + s
self.amount = (BigDecimal(s) * 100000000.0).to_i
else
self.amount = 0
end
end
def amount_in_stc
amount / 100000000.0 unless amount.nil?
end
Dodatkowy warunek jest na poprawność stringów bez zera na początku, typu “.1” i “,21”.
Tylko w tym rozwiązaniu martwi mnie nieco, że wszystkie wartości amount_in_stc w formularzu są konwertowane na prawidłowe liczby (tzn. bzdury wracają jako “0.0” a “1,2” jako “1.2”).