Feature #16021
closed
floor/ceil/round/truncate should accept a :step argument
Added by Dan0042 (Daniel DeLorme) over 5 years ago.
Updated about 5 years ago.
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
- 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?
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.
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
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.
- Description updated (diff)
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.
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.
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.
Also available in: Atom
PDF
Like0
Like0Like0Like0Like0Like0Like0Like0Like0