Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-values] Let B default to 1 in round(<strategy>?, A, B) #9668

Closed
Loirooriol opened this issue Dec 2, 2023 · 19 comments
Closed

[css-values] Let B default to 1 in round(<strategy>?, A, B) #9668

Loirooriol opened this issue Dec 2, 2023 · 19 comments

Comments

@Loirooriol
Copy link
Contributor

I get the reasoning for requiring a precision in most cases. However, as I mentioned in #2513 (comment), it seems fine to omit it for numbers.

So if B becomes optional, defaulting to 1, it will be possible to do things like

round(2.3) /* 2 */
round(up, 2.3) /* 3 */

while round(2.3em) will continue being invalid due to the type mismatch between A and B.

@Loirooriol Loirooriol added the css-values-4 Current Work label Dec 2, 2023
@SebastianZ
Copy link
Contributor

while round(2.3em) will continue being invalid due to the type mismatch between A and B.

Does it have to be invalid? Isn't it possible to just assume the same type for B in that case? I.e. round(2.3em) would be equivalent to round(2.3em, 1em).

Sebastian

@Loirooriol
Copy link
Contributor Author

Loirooriol commented Dec 4, 2023

No, that would be bad. Let's say 1em = 16px, then round(2.3em) should produce the same as round(36.8px). But round(2.3em, 1em) is 2em = 32px, while round(36.8px, 1px) is 37px. Making it unit-dependent is also not clear for round(2.3em 2.3px).

It could work by saying that step defaults to one canonical unit, but in round(2.3em) authors may get surprised it's 1px instead of 1em, so better require it.

@tabatkins
Copy link
Member

Right, that's precisely why it requires a unit right now - we can't have the unit depend on the input (for the reasons given, plus things like round(1em 10px) that don't have a single unit), and we don't want to default a mystery unit that might violate author expectations.

So if B becomes optional, defaulting to 1

Yes, we could do this. Is there a particular reason to do so, tho?

@Loirooriol
Copy link
Contributor Author

It just seems strange to require the 1 when working with plain numbers, since it's the obvious choice. Like, in English people just say "round x", not "round x to a multiple of 1".

@SebastianZ
Copy link
Contributor

Let's say 1em = 16px, then round(2.3em) should produce the same as round(36.8px).

Converting the unit before rounding is totally unexpected from an author's standpoint.

we can't have the unit depend on the input (for the reasons given, plus things like round(1em 10px) that don't have a single unit), and we don't want to default a mystery unit that might violate author expectations.

The point was for rounding when only having a single unit. So, round(1em 10px) would still be invalid. Only round(2.3em), round(42.8deg), or round(87.39%) would be valid (being equivalent to round(2.3em, 1em), round(42.8deg, 1deg), and round(87.39%, 1%), respectively).

Allowing to omit B for numbers but not dimensions or percentages may cause confusion.

Like, in English people just say "round x", not "round x to a multiple of 1".

Not just spoken languages but also also in many programming languages you can omit the precision - or even don't have a way to define one like in JavaScript.

Sebastian

@jfkthame
Copy link
Contributor

jfkthame commented Dec 6, 2023

The point was for rounding when only having a single unit. So, round(1em 10px) would still be invalid.

Would round(3.14159em 2.71828em) be valid, then? The unit is consistent/clear, but it seems like this could still be tricky/confusing. It looks "obvious" what to do when the units are right there, but what about when you have round(--var(foo) --var(bar)), where foo and bar are both lengths? Whether the round works (and what it means) then depends on how they were defined in this particular context. I think that's getting too obscure and footgun-ish to be useful.

Making the precision default to 1 (as a number) only, in contrast, is clear and simple, while rounding anything other than a number requires being explicit about the unit of rounding.

@tabatkins
Copy link
Member

So, round(1em 10px) would still be invalid. Only round(2.3em), round(42.8deg), or round(87.39%) would be valid (being equivalent to round(2.3em, 1em), round(42.8deg, 1deg), and round(87.39%, 1%), respectively).

That's pretty fragile, and it means that round(var(--foo)) may or may not be valid, and might round to an unpredictable precision, based on how the value is written somewhere else.

And round(10em / 3)? Either that's invalid too (and the entire feature starts getting super fragile), or we have to rewrite the simplification rules to maintain units as-is when possible.

In-flight edit: lol, @jfkthame makes the exact same points.


Anyway, if impls want to change the defaulting rules to allow a default of 1 for B, I'm fine with that. Agenda to discuss it.

@Crissov
Copy link
Contributor

Crissov commented Dec 7, 2023

I agree with @SebastianZ and @Loirooriol that it would be unintuitive for authors if the rounding base defaulted to 1<canonical unit>, especially for simple terms like 2.3em, but it might still be the most reasonable and useful solution, because you hardly ever round simple terms.
You usually need either the result of a calculation rounded with the resulting unit or a value with a relative unit, e.g. em or %, expressed as an integer amount in an absolute unit like px.

My educated guess is that the most common use case by far will be rounding (calculated) lengths to whole pixels. Since px is the canonical unit there, this would be covered nicely.

Another viable solution I see would be to treat a <number> given as the second parameter not as a module but as the number of decimal places. Then, round(4.32, 1) would compute to 4.3 instead of 4 and likewise round(4.32em, 1) = 4.3em, while round(4.32em, 1em) would yield 4em. The default value would be 0.

Finally, instead of defaulting to 1, 1<resulting unit> or 1<canonical unit>, the second parameter could support keywords:

  • round(4.32em, auto) = 4em
  • round(4.32em, default) = e.g. 69px
  • round(4.32em, none) = e.g. 69.12px

@tabatkins
Copy link
Member

I strongly disagree that rounding to "whole pixels" is the most common case for rounding lengths. I think that's actually an extremely rare case; subpixel lengths are usually just fine to work with. Similarly, rounding to the nearest degree doesn't seem particularly useful for most cases. The canonical unit is quite arbitrary, and generally speaking I don't think people will commonly be rounding to 1 of any unit; the rounding precision depends intimately on what one is actually trying to accomplish.

the number of decimal places.

That's just a less powerful version of the current argument, where you're limited to precisions that are a power of 10 of some unit. It also has all the existing problems of a default rounding precision.

keywords

These either continue to have the existing problems of a default rounding precision, or are equivalent to just providing a trivial value.

@SebastianZ
Copy link
Contributor

That's pretty fragile, and it means that round(var(--foo)) may or may not be valid

That's true in any case, as --foo can hold anything.

Though we could also define that the unit of the first operand is used. So round(3.14159em 2.71828em) = round(3.14159em 2.71828em, 1em) = 6em, round(1em 10px) = round(1em 10px, 1em) = 2em, and round(10px 1em) = round(10px 1em, 1px) = 26px (in case 1em = 16px).
That makes it explainable and less fragile.

and might round to an unpredictable precision, based on how the value is written somewhere else.

The precision is not unpredictable, it is unit-based.

Similarly, rounding to the nearest degree doesn't seem particularly useful for most cases. The canonical unit is quite arbitrary, and generally speaking I don't think people will commonly be rounding to 1 of any unit.

The point is to provide a reasonable fallback / default. Authors will still be advised to provide a rounding interval if they want to influence the rounding precision.

Another viable solution I see would be to treat a <number> given as the second parameter not as a module but as the number of decimal places.

Note that Firefox already ships the round() function. So, a change in behavior will break any current usages.

Sebastian

@Loirooriol
Copy link
Contributor Author

Though we could also define that the unit of the first operand is used

This would violate commutativity. And in fact serialization sorts summation operands alphabetically by their units, so this wouldn't roundtrip. And anyways it's seems very unintuitive to me.

@tabatkins
Copy link
Member

Ultimately, trying to infer a rounding precision from the source unit is going to fail in lots of cases, and even when it works, there's zero guarantee that it'll produce a useful value.

The current spec makes it trivial to round to any unit you like (round(..., 1em)) and to any size at all, and doesn't need to do any fragile or unexpected inference at all. It's really unlikely we'd ever change it to be more complicated and fragile, for what is almost certainly a small minority use-case of "people who want to round to 1px/1deg/etc".

@Crissov
Copy link
Contributor

Crissov commented Dec 9, 2023

It just occurred to me that with keywords in the second parameter, round() could also be a place to implement truly physical length measurements. #5986 #614

Either round(100mm, physical) or round(100mm, millimeter) would give the best approximation of 1 dm the browser can do with the active output device.

However, authors would probably want arbitrary precision there, which would be crude to support with keywords and always remain incomplete. Treat this as a devil’s advocate proposal then.

@SebastianZ
Copy link
Contributor

It just occurred to me that with keywords in the second parameter, round() could also be a place to implement truly physical length measurements. #5986 #614

That's out of the scope of this issue, which is to make B optional in certain cases.
So @Crissov, you might open a new issue to discuss your proposal. Though given https://wiki.csswg.org/faq#real-physical-lengths, before discussing any syntax proposals, you'd first have provide a solution for how to actually address physical sizes.

Sebastian

@Crissov
Copy link
Contributor

Crissov commented Dec 10, 2023

That is exactly why I did not open another issue. My idea was that if we were to add keywords to the last argument in order to have one of them as an omitable default, we could shoehorn this in as well.

By the way, I reread the original issue #2513, where much of this thread had already been discussed, and I rediscovered #4440 which I totally had forgotten about. It is related but also quite different.

@LeaVerou
Copy link
Member

LeaVerou commented Jan 31, 2024

Strong 1 to defaulting B to 1. It makes sense to require the unit for anything with a unit, and there is no widespread precedent for that in other languages. However, writing round(8.5, 1) is incredibly weird.

(while we're at it, why are we using commas here? It doesn't match the "we use commas only for repetition" pattern. How about round(A [ by B ]?)?)

@tabatkins
Copy link
Member

We use commas mostly for repetition, and sometimes for more general argument separation when the grammars would be ambiguous or at least unclear for authors to be adjacent. That latter reason is what applies here (and for all the math functions) - the arguments aren't numbers, they're calculations, and the bounds of a calculation aren't super clear visually on their own.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-values] Let B default to 1 in `round(<strategy>?, A, B)` .

The full IRC log of that discussion <Frances> Alan: Moving onto the next issue, we are limited by compat concerns.
<Frances> Tab: Round function takes a rounding strategy, we require the modulus to be rounded. We could omit the modulus.
<emilio> q
<lea> q?
<astearns> ack emilio
<lea> q
<Frances> Emilio: It feels weird to make lengths invalid and not make it optional, such as rounding a pixel value. Default for the length would be nice.
<astearns> ack lea
<Frances> Tab: Rounding to one pixel as a default is not obvious, people might not care about subpixels, but rather the unit they are using.
<bramus> 1 on what tab just said.
<emilio> q
<astearns> ack emilio
<Frances> Lea: Should not have to specify 1, whereas dimensions such as length, would make more sense on what you are rounding by, pixels might not be used too much today. Would like default for lengths, possibly not pixels.
<lea> qq to reply to emilio
<Frances> Emilio: In length there is a single unit that other lengths end up computing to.
<astearns> ack lea
<Zakim> lea, you wanted to react to emilio to reply to emilio
<Frances> Lea: Sensible defaults are great when centered around user needs or could be confusing.
<oriol> q
<Frances> Alan: It is reasonable to have numbers default to 1, but not reasonable for numbers to default to invalid for another type.
<Frances> Lea: Which invalid results?
<oriol> q-
<lea> q?
<Frances> Alan: Once taking the pattern of omitting the argument and applying it, it could create invalid creations.
<florian> q
<bramus> q
<emilio> q
<Frances> Lea: When rounding numbers will be clear, when rounding lengths will not be clear.
<emilio> ack emilio
<lea> q
<astearns> ack Frances
<astearns> ack florian
<Frances> Alan: There could be a reasonable default, omit the parameter in some implications but not others.
<oriol> 1 to Florian
<Frances> Florian: Figuring out more syntax is necessary, you need to express something. Figuring out which syntax is implied. For rounding things, separate digits after the comma. Does 10 mean 10 digits after the period or before the period? For lengths, have to think about this stuff in what you mean.
<astearns> ack bramus
<lea> 1 to florian, that's exactly what I was saying too (but always good to have multiple framings)
<lea> q?
<Frances> Bramus: If the default only cares for one of the cases, then it's not a good default?
<Frances> Alan: It could make it difficult for an author to reason whether to use the parameter or not.
<astearns> ack lea
<Frances> Lea: If it's the concern, round could possibly not have a default. It takes a parameter to round by.
<Frances> Alan: We can likely resolve on making the change.
<Frances> PROPOSAL: Allow the modulus parameter to be optional in the case of rounding numbers and default to 1
<Frances> Alan: Objections? No objections.

@bramus
Copy link
Contributor

bramus commented Feb 28, 2024

This got cut off from the meeting notes, as the switch to the next topic happened a bit too soon, but we resolved on the PROPOSAL as listed in the meeting notes:

RESOLVED: Allow the modulus parameter to be optional in the case of rounding numbers and default to 1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Unsorted regular
Development

No branches or pull requests

8 participants