Project

General

Profile

Feature #16021

floor/ceil/round/truncate should accept a :step argument

Added by Dan0042 (Daniel DeLorme) 5 months ago. Updated 3 months ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:93917]

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

History

Updated by shyouhei (Shyouhei Urabe) 4 months 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) 4 months 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) 4 months 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) 4 months 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.

#5

Updated by Dan0042 (Daniel DeLorme) 4 months ago

  • Description updated (diff)

Updated by akr (Akira Tanaka) 3 months 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) 3 months 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) 3 months 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.

Also available in: Atom PDF