Skip to content

[css-values] mod(−0, +∞) and mod(+0, −∞) not handled correctly #4723

Closed

Description

@Loirooriol

From https://drafts.csswg.org/css-values-4/#round-infinities

In mod(A, B) only, if B is infinite and A is non-zero and has opposite sign to B, the result is NaN.
Note: All other "infinite B" cases are valid, and just return A immediately.

This means that mod(−0, +∞) = −0 and mod(+0, −∞) = +0.

However, the returned value is supposed to be between zero and B, with

if B is positive the range starts at 0⁺, and if B is negative it starts at 0⁻.

So we should have mod(−0, +∞) = +0 and mod(+0, −∞) = −0.

Activity

  1. jrus commented on Feb 5, 2020

    @jrus

    mod(−0, +∞) = +0 is arguably invalid. Should be ∞ or NaN instead.

    But really, this shows why mod(x, ∞) is nonsensical in general. There’s no convention you can choose where the result won’t lead to confusing bugs.

    If you just make mod(x, ∞) always NaN it would save trouble.

  2. tabatkins commented on Feb 6, 2020

    @tabatkins
    Member

    It definitely shouldn't be infinity. Replace ∞ with a very large finite number, and the answer is definitely just +0. It could be NaN, but the general policy is that we define the behavior for ∞ arguments to be the limit as the argument approaches ∞, if the limit exists; it's only NaN if there's no limit.

  3. Loirooriol commented on Feb 6, 2020

    @Loirooriol
    ContributorAuthor

    Using limits, shouldn't we have

    mod(finite negative, +∞) = +∞
    mod(finite positive, −∞) = −∞
    

    instead of NaN?

    However, I'd expect mod(A1, B) = mod(A2, B) to imply that A1 - A2 is an integer multiple of B.
    This doesn't hold with the above. Maybe that's what @jrus meant with "mod(x, ∞) is nonsensical in general"?

    But I guess not having that property can be attributed to precision problems. Like

    mod(-1, 1e17) = -1 + 1e17 = 1e17 = -2 + 1e17 = mod(-2, 1e17)
    

    even though (-1) - (-2) = 1 is not an integer multiple of 1e17. So not exclusive for ∞.

  4. jrus commented on Feb 8, 2020

    @jrus

    mod(-0, 5) should arguably be 5 rather than 0. Remember, -0 represents negative underflow, i.e. any real number which is less than zero but greater than the smallest otherwise representable negative number. When you map this into the range 0..5, what you should get is a number which is very slightly less than (but not possible to represent as a float less than) 5. Making mod(-0, 5) == 0 leads to category errors.

    For example, cotpi(x) should equal cotpi(mod(x, 1)), but while cotpi(-0) = cotpi(1) = –∞, cotpi(0) = ∞


    But mod(x, ∞) for any x is nonsensical either way; it implies dividing by ∞, taking the floor, multiplying the resulting quotient by ∞, and then subtracting. We get ∞ · 0 in the middle of that process.

  5. Loirooriol commented on Feb 8, 2020

    @Loirooriol
    ContributorAuthor

    @jrus You are saying -0 ≡ 5 (mod 5), but 5 ≡ +0 (mod 5), so -0 ≡ +0 (mod 5).

    Unlike rem(), the nice property of mod() is that it always returns the same value for the same equivalence class. Breaking this for -0 doesn't seem worth it. We would also loose idempotency.

    What's cotpi?

    But mod(x, ∞) for any x is nonsensical either way; it implies dividing by 0.

    Not necessarily. What mod(A, B) does is it projects A ∈ ℝ into its equivalence class [A] ∈ ℝ / Bℤ, and then takes it back to ℝ by choosing the only representative of [A] between 0 and B (i.e. in the interval [0, B) or (B, 0], depending on the sign of B). There is no division.

    If B→+∞, Bℤ basically becomes {-∞, 0, +∞}. So [A] = {A} (ignoring infinities since it's not much well-defined). Then,

    • For positive finite A, it seems clear that mod(A, +∞) = A, since [A] has no other representative.
    • For negative finite A, we could also say mod(A, +∞) = A. But then we would loose the property that mod(A, B) must be between 0 and B. So NaN is reasonable, meaning that it's not possible to get a representative of [A] in the interval [0, +∞).
  6. jrus commented on Feb 9, 2020

    @jrus

    That floating point arithmetic is not the same as real-number arithmetic is sort of my point. It tends to break down at edge cases.

    I think it’s fine to make mod(-0, 5) return 0 as long as that is documented, and I am not arguing that should be changed, even though this is in some contexts going to cause categorical errors. I am just pointing out that the alternative interpretation about what to do at the edge case is also arguably valid for some situations; whatever choice you pick is going to cause a violation of expectations and some failures in calculations done without careful reading of the specification.

    -0 ≡ 5 (mod 5) is a somewhat strange notation, since modular arithmetic with this notation as used in mathematics only applies to integers, not real numbers.

    Sorry I said “divide by 0”, what I meant was “multiply 0 by ∞”, as I fixed slightly afterward. cotpi(x) = cot(πx), the cotangent of π times the argument. Trigonometric functions with a factor of π included can save some grief in many practical situations, since π cannot be exactly represented in floating point.

    A clearer way to state my original point is: I can’t think of any practical computation where a person would ever end up with mod(x, ∞). I don’t think it really makes much sense. We can’t break the real number line into locally-metric-preserving equivalence classes with 0 ≡ ∞.

    If you define it via a limiting argument, then clearly mod(x, ∞) has a limit of x for any x ≥ 0 and a limit of ∞ for any x < 0. But I think it would be better to just let mod(x, ∞) = NaN regardless.

  7. Loirooriol commented on Feb 10, 2020

    @Loirooriol
    ContributorAuthor

    modular arithmetic with this notation as used in mathematics only applies to integers

    Fair enough, I had only seen it with integers, but I thought I could generalize it.

    cotpi(x) = cot(πx), the cotangent of π times the argument.

    Oh I didn't know this notation. I see your point, but floating point numbers don't have a 1⁺ and a 1⁻. So cotpi(1) could be either +∞ or −∞. See related #4101 about tan(). tan(x + 180deg) = tan(x) but in infinite cases we decided on tan(-90deg) = −∞ and tan(90deg) = +∞.

    I can’t think of any practical computation where a person would ever end up with mod(x, ∞)

    Sure, I don't think people will use an explicit ∞, but possibly they will use some calculation which may end up being ∞ in certain cases.

    I think it would be better to just let mod(x, ∞) = NaN regardless.

    NaN is reasonable for negative x, but not that sure about positive x. Intuitively I would expect x, though I could live with NaN.

  8. tabatkins commented on Oct 21, 2021

    @tabatkins
    Member

    Re-reviewing this, I think I agree that the current behavior (mod(-0, infinity) yielding -0) is indeed wrong. I believe there are two reasonable approaches here:

    • Flip it to the appropriately-signed zero instead, just like mod(-0, 5) yield +0; while we can't tell what value a mod(-1, infinity) should resolve to, "0 away from infinity" is just infinity and thus in the 0 equivalence class ^_^
    • Pay attention to sign first, so a wrong-signed zero yields NaN like any other wrong-signed value would.

    I'm relatively ambivalent between the two, but weakly prefer the second; a process which approaches -0 is doing so from the negative side, where all the similar non-zero negative values produce NaN, and so I think it's slightly better to be consistent there.

    Thoughts?

  9. Loirooriol commented on Oct 26, 2021

    @Loirooriol
    ContributorAuthor

    Both approaches can be reasonable, no strong opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions