Feature #16021
closedfloor/ceil/round/truncate should accept a :step argument
Description
These rounding methods currently accept a number of (decimal) digits, but a more general mechanism would allow rounding to the nearest ΒΌ, multiple of 5, etc.
Equivalent to e.g. ((num / step.to_f).round * step)
12.3456.floor(step: 0.2) #=> 12.2
12.3456.round(step: 0.2) #=> 12.4
12.3456.floor(step: 0.2) #=> 12.4
12.3456.floor(step: 0.2) #=> 12.2
IMHO this should also apply to Time#floor/round/ceil
Time.now.floor(step: 3600) #=> current hour
Time.now.round(step: 3600) #=> nearest hour
Time.now.ceil(step: 3600) #=> next hour
We can also consider that instead of :step
, :by
or :to
might be quite readable.
12.3456.round(by: 0.2) #=> 12.4
12.3456.round(to: 0.2) #=> 12.4
Updated by shyouhei (Shyouhei Urabe) over 5 years ago
- Status changed from Open to Feedback
Dan0042 (Daniel DeLorme) wrote:
Equivalent to e.g.
((num / step.to_f).round * step)
12.3456.floor(step: 0.2) #=> 12.2
Can I ask you what do you expect for 12.3456.floor(step: 0.0002)
then?
Updated by shevegen (Robert A. Heiler) over 5 years ago
Hmm. I have mixed feelings about the proposal. I think in principle it
would be ok to add more flexibility as such (e. g. :step or :by, although
I think :step is a strange name). At the same time, though, this proposal
makes the use of .floor() etc... a bit more complicated. People have to
think more than before, such as by the example shown by shyouhei.
12.3456.floor(step: 0.0002)
versus
12.3456.floor(3)
(Not the same, I know, but my point is mostly that the second usage is
much easier to understand from a glance alone.)
I do not want to be discouraging but personally I more prefer to retain
the current way only.
Note that I have no really substantial opinion on the Time.now examples
given above - haven't thought about its usage implications yet. I mostly
refer to e. g. .floor() etc... on numbers.
Updated by Dan0042 (Daniel DeLorme) over 5 years ago
shyouhei (Shyouhei Urabe) wrote:
Dan0042 (Daniel DeLorme) wrote:
Equivalent to e.g.
((num / step.to_f).round * step)
12.3456.floor(step: 0.2) #=> 12.2
Can I ask you what do you expect for
12.3456.floor(step: 0.0002)
then?
Good point, I see what you mean. I would expect 12.3456 but because of float imprecision we get 12.3454 using the above formula. Of course that's the gotcha with any float operations. Maybe I should have said logically equivalent to ((num / step.to_f).round * step)
There are implementation workarounds, e.g.
(12.3456 / 0.0002).next_float.floor * 0.0002 #=>12.345600000000001
(12.3456 / 0.0002) .round * 0.0002 #=>12.345600000000001
(12.3456 / 0.0002).prev_float.ceil * 0.0002 #=>12.345600000000001
Or the new, magical fix_float
method! :-) (half-jesting)
class Float
def fix_float(sensitivity=6)
n = next_float
return n if n.to_s.size < self.to_s.size - sensitivity
p = prev_float
return p if p.to_s.size < self.to_s.size - sensitivity
return self
end
end
(12.3456 / 0.0002) #=> 61727.99999999999
(12.3456 / 0.0002).fix_float #=> 61728.0
((12.3456 / 0.0002).fix_float.floor * 0.0002).fix_float #=>12.3456
Updated by Dan0042 (Daniel DeLorme) over 5 years ago
shevegen (Robert A. Heiler) wrote:
(Not the same, I know, but my point is mostly that the second usage is
much easier to understand from a glance alone.)
Then what about making them equivalent so it's easier to compare which is more understandable.
12.3456.floor(step: 0.0002)
versus
(12.3456 / 0.0002).floor * 0.0002
IMHO the intent of the first one is obvious even without reading the floor
documentation (but of course YMMV). If you think the second one is more understandable then my proposal has no leg to stand on.
Although as shyouhei pointed out, the pitfalls of float arithmetic mean that solving those pitfalls gives this proposal more value than just syntactic sugar alone.
Updated by Dan0042 (Daniel DeLorme) about 5 years ago
- Description updated (diff)
Updated by akr (Akira Tanaka) about 5 years ago
Time.now.floor(step: 3600)
doesn't work well with leap seconds.
Also, we want floor/round/ceil for month and year
but one month and one year is not fixed number of seconds.
So, step with number of seconds doesn't for month and year.
Updated by matz (Yukihiro Matsumoto) about 5 years ago
As @akr (Akira Tanaka) stated, round
etc.for Time
class do not work well (rejected).
For float values, we could suffer from errors. Unless there're real-world use-cases, I am not positive.
Matz.
Updated by Dan0042 (Daniel DeLorme) about 5 years ago
Time.now.floor(step: 3600)
doesn't work well with leap seconds.
n = (Time.now.to_i / 86400).floor * 86400
ENV["TZ"] = "UTC"; Time.at(n) #=> 2019-09-19 00:00:00 +0000
ENV["TZ"] = "right/UTC"; Time.at(n) #=> 2019-09-18 23:59:33 +0000
Wow. I thought that leap seconds were handled by the OS by repeating the same unix timestamp twice, or freezing or fudging time. To think that the TZ would change the meaning of a timestamp in such a way... I learned something quite interesting today. (Thanks #8885 btw)
For float values, we could suffer from errors.
That's always the case for any float operations right? 12.3456.floor(4)
=> 12.3455
It's possible to fix precision errors (for floor/ceil), but is it desirable?
But since the proposal is rejected for Time, it's much less relevant for Numeric in general. It's ok to close this.