I want to convert a subtitle time code:
begin="00:00:07.71" dur="00:00:03.67
to pure seconds:
begin=7.1 end=11.38
I wrote a Ruby code:
def to_sec(value)
a = value.split(':')
a[0].to_i*3600+a[1].to_i*60+a[2].to_f
end
which resulted in 11.379999999999999.
Can anybody tell me why this happens?
Is there any Time library that can do this conversion?
It'll probably be easiest for you to represent your underlying datatype as integer hundredths of a second (centiseconds):
def to_csec(value) #if you had CSec < Integer this would be `def self.[](value)`
a = value.split(':')
#tacking on a couple zeros to each
a[0].to_i*360000+a[1].to_i*6000+(a[2].to_f * 100).to_i
end
You could add some helpers for dealing with the durations and pretty printing them as well:
def csec_to_s(csec) #if you had CSec < Integer, this would be `def to_sec`
"%.2f" % (csec.to_f / 100)
end
class SubtitleDuration < Range
def initialize(a,b)
centi_a = to_csec(a)
super(centi_a,to_csec(b) + centi_a)
end
def to_s
"begin=#{csec_to_s(self.begin)} end=#{csec_to_s(self.end) }"
end
end
Then your answer is just:
puts SubtitleDuration.new("00:00:07.71", "00:00:03.67").to_s
#=> begin=7.71 end=11.38
This sort of thing can happen in just about any programming language. It's because of how floating point numbers are represented. They're not stored as decimals under the hood, so sometimes you get odd rounding errors like this.
Related
A follow up on the question How to create a random time between a range
.
Kernel#rand works with Time range:
require 'time'
rand(Time.parse('9 am')..Time.parse('11:30 am'))
But when I tried with a custom class, I ended up with the error:
`rand': no implicit conversion of Range into Integer (TypeError)
class Int
include Comparable
attr_reader :num
def initialize(num)
#num = num
end
def succ
Int.new(num + 1)
end
def <=>(other)
num <=> other.num
end
def to_s
"Int(#{num})"
end
def to_int
#num
end
alias_method :inspect, :to_s
end
puts rand(Int.new(1)..Int.new(3))
Why? What am I missing in the custom class? Can we use such a custom class in rand(Range)?
I don't know of any documentation for what specifically Kernel#rand expects from a Range argument but we can get a look at what's going on by overriding respond_to? in your class and then watching as things fall apart:
def respond_to?(m)
puts "They want us to support #{m}"
super
end
Doing that tells us that rand wants to call the #- and #+ methods on your Int instances. This does make some sense given that rand(a..b) is designed for working with integers.
So we throw in quick'n'dirty implementations of addition and subtraction:
def -(other)
self.class.new(to_int - other.to_int)
end
def +(other)
self.class.new(to_int + other.to_int)
end
and we start getting rand Ints out of our calls to rand.
I'm not sure where (or if) this is documented so you'll have to excuse a bit of hand waving. I normally spend some time rooting around the Ruby source code to answer this sort of question but I lack the time right now.
To add a bit more to #mu-is-too-short's answer, I checked the source of Random#rand and the following is the current implementation logic for rand(Range):
Get the begin, end, and vmax from the Range object (call range_values), where vmax is computed as (call id_minus):
vmax = end - begin
vmax will be used as the upper bound of the random number generation later.
This requires the custom class to have - method defined.
Generate a random number based on the type of vmax:
If it is not Float and can be coerced to Integer (rb_check_to_int), generate a random Integer less than vmax.
In this case, the - method should either return an Integer, or an object which responds to to_int method.
If it is Numeric and can be converted to Float with to_f, (rb_check_to_float), generate a random Float number less than vmax.
In this case, the - method should return a Numeric number which can be converted to Float with method to_f.
Add the random number to begin to yield the result (call id_add).
This requires the custom class to have + method defined, which accepts the result of the random number generated in step 2 (either Integer, or Float) and returns the final result for rand.
I believe this error is because you are trying to use rand() on objects of your custom class.
`rand': no implicit conversion of Range into Integer (TypeError)
This error message clearly mentions that ruby was unable to convert your range into integer. Based on your code snippet, following works and might be what you are looking for.
puts rand(Int.new(1).to_int..Int.new(3).to_int)
I'm testing a small and simple library I made in Ruby. The goal is to convert from EUR to CNY and vice versa. Simple.
I tested it to be sure everything works but I got an unexpected issue. When I use to_euro followed by to_yuan it should go back to the original amount ; it doesn't happen. I tried to .to_f or round(2) the amount variable which fix some tests, raise new ones, but it's never equal to what I expect globally ; I'm running out of idea to fix this :(
class Currency
attr_reader :amount, :currency
def initialize(amount, currency='EUR')
#amount = amount
#currency = currency
end
def to_yuan
update_currency!('CNY', amount * Settings.instance.exchange_rate_to_yuan)
end
def to_euro
update_currency!('EUR', amount / Settings.instance.exchange_rate_to_yuan)
end
def display
"%.2f #{current_symbol}" % amount
end
private
def current_symbol
if currency == 'EUR'
symbol = Settings.instance.supplier_currency.symbol
elsif currency == 'CNY'
symbol = Settings.instance.platform_currency.symbol
end
end
def update_currency!(new_currency, new_amount)
unless new_currency == currency
#currency = new_currency
#amount = new_amount
end
self
end
end
Tests
describe Currency do
let(:rate) { Settings.instance.exchange_rate_to_yuan.to_f }
context "#to_yuan" do
it "should return Currency object" do
expect(Currency.new(20).to_yuan).to be_a(Currency)
end
it "should convert to yuan" do
expect(Currency.new(20).to_yuan.amount).to eql(20.00 * rate)
end
it "should convert to euro and back to yuan" do
# state data test
currency = Currency.new(150, 'CNY')
expect(currency.to_euro).to be_a(Currency)
expect(currency.to_yuan).to be_a(Currency)
expect(currency.amount).to eql(150.00)
end
end
context "#to_euro" do
it "should convert to euro" do
expect(Currency.new(150, 'CNY').to_euro.amount).to eql(150 / rate)
end
end
context "#display" do
it "should display euros" do
expect(Currency.new(10, 'EUR').display).to eql("10.00 €")
end
it "should display yuan" do
expect(Currency.new(60.50, 'CNY').display).to eql("60.50 ¥")
end
end
end
And here's my RSpec result
I'm pretty sure this problem is very common, any idea how to solve it easily ?
Float isn't an exact number representation, as stated in the ruby docs:
Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.
This not ruby fault, as floats can only be represented by a fixed number of bytes and therefor cannot store decimal numbers correctly.
Alternatively, you can use ruby Rational or BigDecimal
Its is also fairly common to use the money gem when dealing with currency and money conversion.
I am getting the following rounding error when I try to Unit test the class below:
class TypeTotal
attr_reader :cr_amount, :dr_amount,
:cr_count, :dr_count
def initialize()
#cr_amount=Float(0); #dr_amount=Float(0)
#cr_count=0; #dr_count= 0
end
def increment(is_a_credit, amount, count=1)
case is_a_credit
when true
#cr_amount = Float(amount)+ Float(#cr_amount)
#cr_count += count
when false
#dr_amount = Float(amount)+ Float(#dr_amount)
#dr_count += count
end
end
end
Unit Test:
require_relative 'total_type'
require 'test/unit'
class TestTotalType < Test::Unit::TestCase
#rounding error
def test_increment_count()
t = TypeTotal.new()
t.increment(false, 22.22, 2)
t.increment(false, 7.31, 3)
assert_equal(t.dr_amount, 29.53)
end
end
Output:
1) Failure:
test_increment_count(TestTotalType) [total_type_test.rb:10]:
<29.529999999999998> expected but was
<29.53>.
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
I am using Floats because it was recommended in the Pick Ax book for dollar values because they shouldn't be effective by round errors.
I am running on ruby 1.9.2p290 (2011-07-09) [i386-mingw32] on Windows 7 64-bit Home and Windows XP 32-bit Pro.
I've tried
casting my variables to floats
removing += and spelling out increment
The behavior appears random:
12.22 + 7.31 works
11.11 + 7.31 doesn't work
11.111 + 7.31 works
Any ideas whats going wrong?
Are you sure that was the advice given? I'd expect the advice to be not to use Floats, precisely because they use binary floating point arithmetic, and so are prone to rounding errors. From the Float documentation:
Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.
If you could quote the exact advice you're referring to, that would help.
I would suggest you use BigDecimal instead, or use an integer with implicit units of "cents" or "hundreds of a cent" or something similar.
The problems with float are already mentioned.
When you test with floats, you should not use assert_equal but assert_in_delta.
Example:
require 'test/unit'
class TestTotalType < Test::Unit::TestCase
TOLERANCE = 1E-10 #or another (small) value
#rounding error
def test_increment_count()
t = TypeTotal.new()
t.increment(false, 22.22, 2)
t.increment(false, 7.31, 3)
#~ assert_equal(t.dr_amount, 29.53) #may detect float problems
assert_in_delta(t.dr_amount, 29.53, TOLERANCE)
end
end
The solution was to use Big Decimal:
require 'bigdecimal'
class TypeTotal
attr_reader :cr_amount, :dr_amount,
:cr_count, :dr_count
def initialize()
#cr_amount=BigDecimal.new("0"); #cr_count=0,
#dr_amount=BigDecimal.new("0"); #dr_count=0
end
def increment(is_a_credit, amount, count=1)
bd_amount = BigDecimal.new(amount)
case is_a_credit
when true
#cr_amount= bd_amount.add(#cr_amount, 14)
#cr_count += count
when false
#dr_amount= bd_amount.add(#dr_amount, 14)
#dr_count = count
end
end
The Pick Ax (p53) book used float for currency as an example but had a foot note explaining that you need to either add .5 cent to when you display the value or use Big Decimal.
Thanks for are your help!
I looking for ruby code to convert BCD to Ascii.I have tried with many codes but i am not getting proper result.
Any suggestions or code samples?
I actually wrote a gem for this sort of thing.
https://rubygems.org/gems/bcd
The source code is at https://github.com/dafyddcrosby/ruby_bcd
Although this question is long dead, I just dealt with this. Assuming you've already converted to bcd:
bcd_val.to_s(16)
This converts the bcd value to hex, but because each nibble is a digit, and all digits are between 0-9, it effectively displays as an integer string.
Note that if your bcd number is stored LSB to MSB (depending on how you converted to bcd) the number will display backwards. At that point you can always user str.reverse as necessary.
It is not too clear what do you want to do, but hope that the following can help:
def to_bcd(n)
str = n.to_s
bin = ""
str.each_char do |c|
bin << c.to_i.to_s(2).rjust(4,'0')
end
bin
end
def to_dec(bcd)
n = ""
(bcd.length / 4).times do |i|
n << Integer('0b'+bcd[i*4..(i*4+3)]).to_s
end
n
end
result = to_bcd(120)
p result #=> "000100100000"
p to_dec(result) #=> "120"
p to_dec(result).to_i.chr #=> "x"
If you like, you could extend the Integer and String class with the two methods above, respectively, without arguments and substituting the parameter name with self. But someone does not like the idea to extend standard classes, because it is not a clean/safe programming habit.
In ruby, I want to convert a float to an int if it's a whole number. For example
a = 1.0
b = 2.5
a.to_int_if_whole # => 1
b.to_int_if_whole # => 2.5
Basically I'm trying to avoid displaying ".0" on any number that doesn't have a decimal. I'm looking for an elegant (or built-in) way to do
def to_int_if_whole(float)
(float % 1 == 0) ? float.to_i : float
end
One simple way to it would be:
class Float
def prettify
to_i == self ? to_i : self
end
end
That's because:
irb> 1.0 == 1
=> true
irb> 1 == 1.0
=> true
Then you could do:
irb> 1.0.prettify
=> 1
irb> 1.5.prettify
=> 1.5
A one liner sprintf...
sprintf("%g", 5.0)
=> "5"
sprintf("%g", 5.5)
=> "5.5"
This is the solution that ended up working the way I want it to:
class Float
alias_method(:original_to_s, :to_s) unless method_defined?(:original_to_s)
def is_whole?
self % 1 == 0
end
def to_s
self.is_whole? ? self.to_i.to_s : self.original_to_s
end
end
This way I can update the is_whole? logic (I seems like tadman's is the most sophisticated) if needed, and it ensures that anywhere a Float outputs to a string (eg, in a form) it appears the way I want it to (ie, no zeros on the end).
Thanks to everybody for your ideas - they really helped.
I'm don't know much about Ruby.
But this is a display issue. I would be extremely surprised if the libraries you are using don't have a way to format a number when you convert it to a string.
There might not be a catch-all formatting option that does exactly what you want but you could set up a method that returns true if the float is the float representation of a whole number and false otherwise. Inside a formatting routine that you create (so you only have to do this in once place) just change the formatting based on if this is true or false.
This discusses how to control the number of digits that appear after the decimal when displaying a number.
Watch out for the intricacies of floating point representations. Math might say the answer is 3 but you may get 3.000000000000000000001. I'd suggest using a delta to see if the number is almost an integer number.
If you are using rails you can use helper number_to_rounded with option strip_insignificant_zeros, for example:
ActiveSupport::NumberHelper.number_to_rounded(42.0, strip_insignificant_zeros: true)
or like this:
42.0.to_s(:rounded, strip_insignificant_zeros: true)
Although I'd tend to agree with the above post, if you must do this:
(float == float.floor) ? float.to_i : float
Here's my horribly hacktastic implementation provided for educational purposes:
class Float
def to_int_if_whole(precision = 2)
("%.#{precision}f" % self).split(/\./).last == '0' * precision and self.to_i or self
end
end
puts 1.0.to_int_if_whole # => 1
puts 2.5.to_int_if_whole # => 2.5
puts 1.9999999999999999999923.to_int_if_whole # => 2
The reason for using the sprintf-style call is that it handles floating point approximations much more reliably than the Float#round method tends to.
I don't know much about Ruby either.
But in C++, I'd do this:
bool IsWholeNumber( float f )
{
const float delta = 0.0001;
int i = (int) f;
return (f - (float)i) < delta;
}
And then I'd format the output precision based on that.