Closed Crissov closed 4 years ago
I think mround(value, multiple)
would be a more intuitive name.
Alternatively, with round
one could use calc(multiple * round(value/multiple))
Though I see the more full featured request which spawned this issue was closed for lack of use cases (https://github.com/w3c/csswg-drafts/issues/905), I am at least encountering sub-pixel issues all the time when trying to create responsive component styles. And they are becoming more and more of an issue if I want to actually utilize recent "wins" on the css unit front.
Not being able to coerce a rounded pixel value from things like this..
--offset: calc(50vw * 0.2 / var(--count));
...often forces me to compute dimensions with javscript, adding hacky resize listeners and thrashing my layout, to then use --offset
in meaningful ways where "hairline" alignments won't compound in situations like...
position: sticky;
left: calc(var(--offset) * 2);
width: calc(100vw - (var(--offset) * 2));
scroll-snap-align: end;
Because the used sub-pixel values here can render the authors intended layout mostly futile.
I find the sub-pixel case as the most pertinent for a rounding solution, though the suggestion from @Crissov for all lengths sounds worthwhile. Especially when I think of 2dppx
/3dppx
screens and nearest .5px
or creating elastic grid layouts where width/height "snap" to multiples of their gutter.
@jonjohnjohnson Thank you for the more concrete example.
@Loirooriol The more CSSy alternative to mod()
would be just round()
. There is no need for mround()
because you would always specify the precision as a module, e.g. round(calc(10px / 3), 0.01px)
. There may be a need or at least demand for ceiling and flooring support, but that can be added (even at a later time) with keywords: round(10.5px, 1px down)
. I don't care much whether the comma should be there or whether space suffices or a keyword (by
, to
, up
, down
…) should take its place. (The comma is necessary for the integrated mode function described in #905.)
I think floor(), ceil(), and round() all have very reasonable use-cases. (And adding any one of them allows one to implement the other two, so might as well add them as a group.)
Unlike in JS, CSS doesn't have a default scale to round to, so all three of the functions will require a precision. The only difference between floor/ceil/round will be which direction is favored when the value is between steps of the precision.
As you say, @Crissov, an alternative syntax would be add a single round() function, and let it have an optional third argument that dictates how to round it, like round(15em, 10px, floor)
or something. It needs to be comma-separated from the precision, since the precision should be a calculation.
Should/could CSS have a default scale for this purpose? Or can these functions locally define a default for an omitted precision parameter?
FWIW, css-rhythm already introduces a set of keywords for rounding behavior: up
(ceil), down
(floor) and nearest
for its block-step-round
property.
@astearns Setting the default scale for <length>
is what #4440 was about.
I'm not sure it would be desirable to assume a fixed default precision of, say, 1 canonical unit, e.g. 1px
.
I think such a function should accept <number>
s or <dimension>
s, but only be valid when the value and the precision have the same type. The most reasonable default for an omitted precision parameter would be 1
. Then you can use round(2.4)
but not round(2.4px)
.
At first glance it can seem that a 1px
precision would be good for round(2.4px)
, but would people still expect 1px
for round(2.4cm)
, or would they expect 1cm
? What about round(2.4cm + 3.5lh)
or round(2.4deg + 3.5rad)
?
To avoid confusion, for dimensions I think it's better to require the precision like round(2.4px, 1px)
. Authors could also do the math with numbers and add the unit at the end: calc(round(2.4) * 1px)
Ah, allowing a default precision for plain numbers, but requiring an explicit one for dimensions, works for me.
And yes, I definitely think that authors would expect round(2.4cm)
to produce 2cm
, which we can't guarantee; we need to require the author the author to say round(2.4cm, 1cm)
.
@Crissov Ooh, good catch on remembering about the Rhythm keywords.
Since some authors are accustomed to Javascript/EcmaScript functions and the CSS WG strives for compatibility where possible and reasonable, here is a list of related standard methods of the Math
object:
round(number) = integer
, round to nearest integer, or towards positive infinity if ambiguous
roundBy(number, integer) = number
, optional second parameter to specify number of decimal placesfround(number) = float-32
, rounding to nearest 32-bit single precision floating point number, or towards the next even number if ambiguousceil(number) = integer
, round up to next integer towards positive infinityfloor(number) = integer
, round down to next integer towards negative infinity
floor(number, integer)
, optional second parameter to specify number of decimal placestrunc(number) = integer
, truncate to integral part, i. e. round towards zero When Tab asked for opinions on Twitter, my first instinct was to favour separate round/ceil/floor rather than the more verbose function+keyword modifier option. And ceil and floor are familiar from JS.
But for people who don't have an imperative programming or mathematical background, maybe they are just more confusing jargon?
If the preference is to have a single function that does all three operations, I'd recommend (a) putting the keyword in front, and (b) using the keywords from CSS Rhythm, so it's more readable as an English phrase: "round up", "round down", "round nearest" (which would still be the default for round):
round() = round([[up|down|nearest|towards-zero|away-from-zero],]? <value>, <step-size>)
(The towards-zero
and away-from-zero
being different from up
and down
in how they affect negative versus positive values. Which is an option ceil and floor don't let you control.)
Another thought: it might be useful to have a third value for specifying an offset/initial value from which to start the step function. For example:
round(var(--count), 2); /* rounds to the nearest even number */
round(var(--count), 2, 1); /* rounds to the nearest odd number */
/* or imagine setting the width of a grid container to neatly fit the child items… */
round(down, 100%, /* take 100% width and round it down */
var(--item-width) + var(--gap-width), /* to a multiple of the total width for an item + gap */
-1 * var(--gap-width) ); /* except that the first item doesn't need a gap, so subtract it */
Another thought: it might be useful to have a third value for specifying an offset/initial value from which to start the step function
I also thought about this, but not sure if it's worth it since it can be trivially achieved with
round(value, precision, offset) = round(value - offset, precision) + offset
Also, if there is an offset, I guess towards-zero and away-from-zero would actually be towards-offset and away-from-offset?
@AmeliaBR:
Putting the keyword first does read better, yeah.
And as Oriol said, I don't think an offset is necessary here; it complicates the grammar with a third unlabeled calculation, which isn't great design, and it's pretty easy to handle yourself in the rare cases it's needed. ("Rare" based on my own usage of rounding across my CS career; I've only needed to do that a handful of times.)
@Crissov:
In this case I think there's good reason to avoid pure JS compat. As stated before, there's no way to do rounding with unitted values without specifying a precision, which already breaks compat somewhat. (I don't think it's reasonable to only allow rounding on numbers; we're currently only applying that restriction on the two functions where it's necessary to do so due to the power changing; everything else is adapted to unitted values appropriately.)
Also, ceil is just a terrible, terrible name. It's difficult for me to remember whether it's ei or ie, and I know I'm not alone. Using the Rhythm keywords reads much better, I think.
fround() doesn't seem necessary; I'm not even really sure what it's purpose is in JS, let alone what one could possible want it for in CSS.
Similarly, trunc() is just "round towards zero"; if we include it we should use the same naming scheme as the others.
For posterity, the results of my Twitter poll are roughly 3:1 in favor of separate functions instead of a single function with keywords.
As usual, polls are far from binding; people's spot opinions don't always reflect their long-term opinions, and there are many constraints in play besides initial preference as well.
I'm not sure I buy the argument that the standard names are confusing in this case.
If the names are new and confusing, I'd assume so is having to think about the different rounding modes at all. If that's the case, writing round(up, var(--x))
for ⌈x⌉ is going to be confusing when --x
is less than 0. At the very least, round(to inf, var(--x))
would be a better middle ground.
In the case of floor and ceiling specifically, I'd also imagine that those either are or will be introduced to more people at an earlier age as math education includes more discrete math as more computer science education is introduced. So, at least for those two, I think there would be a long term benefit for calling them by their more standardized names.
I didn't say the names were confusing, I said they're hard to spell. (Tho Amelia does suggest they may be confusing.) I have to type ciel/ceil every time and see which one works.
I'll note, tho, that I don't actually know offhand what Math.floor(-1.5)
returns. Looks like it's... -2, so it's "round towards -Infinity", yeah.
Well, thatʼs just anecdotal, not empirical, evidence to support a certain decision. English is not my native language and I find the spelling of ceiling not hard at all.
PS: Nevertheless, I'm not really in favor of introducing ceil()
and floor()
as separate functions.
The CSS Working Group just discussed VAlues and units round()/floor()/ceil()/mod() (tab)
, and agreed to the following:
RESOLVED: Adopt a round function with keywords detailing which behavior
RESOLVED: add the mod function with an open issue about behavior
The CSS Working Group just discussed update on mod() function
.
Added round() in 90ff717f0. mod() is waiting on final resolution on naming; I hope to resolve that today.
The CSS Working Group just discussed mod() mode
, and agreed to the following:
RESOLVED: the mod() and rem() functions are to be added to CSS, one with math behavor, the other with JS behavior
So, to clarify:
rem(-8,3)
in CSS will give the same result as -8 % 3
in JS, aka -2 — the difference since the last multiple of 3 when counting away from zero: -8 is -2 beyond -6=3*(-2). The result of rem()
is negative if the first value is negative.
In general, rem(<value>, <step-size>)
equals calc(<value> - round(to-zero, <value>, <step-size>)
mod(-8,3)
in CSS will give +1 — the amount greater than the next smaller multiple of 3 (meaning, closer to negative infinity): -8 is +1 above -9=3*(-3). The result of mod()
is always positive.
In general, mod(<value>, <step-size>)
equals calc(<value> - round(down, <value>, <step-size>)
Exactly correct, yes.
Another way to think of it is, repeatedly add or subtract the absolute value of the step-size to bring the value closer to zero. rem() stops when the value is as close as possible to zero without crossing it; mod() stops when the number is between 0 and the step.
There's a bunch of equivalent formulations.
(Oh yeah, not exactly correct: mod() is not always positive, it's always the sign of the second argument. rem() is always the sign of the first argument.)
mod() is not always positive, it's always the sign of the second argument.
That's not what JS does, at least not as it is implemented in browsers. Nevermind. JS 8%-3
gives me +2 in my Chrome and Firefox consoles.%
is rem()
, not mod()
.
heycam: […] it's confusing to have an unit called rem, and a function called rem
I agree with this concern, but I can't think of a better proposal other than writing out remainder()
in full.
And yes, I definitely think that authors would expect round(2.4cm) to produce 2cm, which we can't guarantee; we need to require the author the author to say round(2.4cm, 1cm).
Why? Why not default to using the unit that is next to the number, so that round(2.4cm)
is the same as round(2.4cm, 1cm)
, round(2.4em)
is the same as round(2.4em, 1em)
, round(2.4)
is the same as round(2.4, 1)
, etc.? Or, put another way, round(2.4cm)
is the same as calc(round(2.4) * 1cm)
. If there are two units, then use the first unit, so that round(2cm + 19px)
is the same as round(2cm + 19px, 1cm)
. This would just be, what do you call it, syntax sugar? But it sure would make it simpler to read and author.
Being able to use single parameters in the most common use cases is an important ease of use consideration. You don’t then need to recall if they are space separated or comma separated, for one thing, or which parameter comes first. Which is also why I favor multiple functions instead of extra parameters. I would much prefer to be able to do this:
round(2.4cm)
round-up(2.4cm)
round-down(2.4cm)
round-from-zero(2.4cm)
round-to-zero(2.4cm)
To me, that is much simpler and easier to remember and understand. I’m not a math major, and it is super clear. In my mind, it is hands down much, much better than this:
round(nearest, 2.4cm, 1cm)
round(up, 2.4cm, 1cm)
round(down, 2.4cm, 1cm)
round(from-zero, 2.4cm, 1cm)
round(to-zero, 2.4cm, 1cm)
Then, if I want to use a different precision, that is the only time I need a second parameter. And if it is a number, use the same unit as the first parameter. So, round(2.4cm, 5)
== round(2.4cm, 5cm)
, and round(2cm + 19px, 10)
== round(2cm + 19px, 10cm)
.
——
I don’t really follow what mod()
or rem()
are (I guess mod()
means modulo?), or when you need them. The abbreviations tend to obscure their meaning. I always thought rem meant “root em” in CSS. Are these included just for completeness, or for people who want CSS to look hard to grok, or what? If rem
is the same thing as round-to-zero
, then what’s the point?
TabAtkins: our goal is to give designers the functions they need to get the layouts they want
Indeed! This needs to be pointed out explicitly sometimes, because itʼs all too easy to just strive for compatibility with JS (or some other language not tailored to styling) in the web ecosystem.
If there are two units, then use the first unit, so that round(2cm + 19px) is the same as round(2cm + 19px, 1cm). This would just be, what do you call it, syntax sugar? But it sure would make it simpler to read and author.
It's actually not, and for some very important reasons.
First and most importantly, the precision you want to round to is virtually never "1 of the unit I'm using". Obviously there's no need to write round(2.4cm)
- you can just write 2cm
in your stylesheet and be clearer. If you're passing in a value from a variable, like round(var(--foo))
, you don't know the unit and can't hand-round it, but then you're getting an unpredictable precision, thus an unpredictable effect on your layout, which is almost certainly not what you want. This is because the point of rounding is to make something a multiple of some base length that's significant to your exact situation. JS's "round to nearest integer" isn't even that useful actually; I'm pretty sure most of my rounding usage in JS looks like Math.round(val / step) * step
; I only use an un-scaled round()
when I'm displaying a value on-screen (and even then, I often use .toFixed()
on the value, which takes a decimal precision).
Second, if there are multiple values, relying on the first one is a footgun. A seemingly-harmless change, like switching from calc(2cm + 5em)
to calc(5px + 2cm + em)
, completely changes the result; if it was written as calc(2cm + 5em, 1cm)
, then changing the value has no such unpredictability.
Third, tracking the types means being aware of exact parsed syntax in a way that no other math function (or anything else in CSS, for that matter) is. In every single other instance, 1em
, 16px
,12pt
, etc are all exactly equivalent. Breaking that correspondence is something we should do only with a very good reason.
And even if we did do so, it would just introduce further confusion - %s are processed late, and are explicitly about their resolved value elsewhere in math functions. (sign()
has an explicit callout about the fact that sign(50%)
might return -1!) And then what about a function? Or an attr() with the unit specified by the keyword? Etc.
The best we could reasonably do for a default precision is to base it on the canonical unit for a given type - round to the nearest px for all lengths, etc. But that's even further removed from any hope of matching the author's intent, per my first point.
Syntax sugar is great when it simplifies a common case, and improves overall readability by removing obvious details. It's bad when it causes more special cases you have to worry about.
Which is also why I favor multiple functions instead of extra parameters.
As your examples show, it's literally just a matter of placing the keyword before or after the (
; there is no other change beyond which punctuation you use to separate it from the surrounding syntax. Preferring one vs the other is a reasonable aesthetic preference, but we need more than that to decide (after all, I personally prefer the consistent look of the common function name + keyword).
We went for the keyword for two reasons: first, as dbaron stated (hopefully captured in the minutes?), there are actually more rounding modes than this, particularly "round to nearest, but resolves ties by X" (instead of always resolving a tie as "up"), and keywords are compatible with adding such control later; second, just in case this is something that an author or library wants to make controllable, a keyword can be controlled via a variable, but part of a function name can't be. (In JS it can, but we don't offer that sort of concatenation and indirection in CSS.)
I don’t really follow what mod() or rem() are (I guess mod() means modulo?), or when you need them. The abbreviations tend to obscure their meaning.
Some variety of modulus operator is common to virtually every programming language in existence. mod
and rem
are very common names for them, as the Wikipedia list shows; the only other common name is %
, which I think you'll agree obscures even more. ^_^ I do explain what the abbreviated names mean in the spec (which I finished on the plane and just now pushed), and how to think about their behavior and their differences.
If you don't know when you'll use mod, that's fine, you don't need it then. But, as the existence of the terrible abuse-the-precision-range-of-doubles hack linked on Twitter shows, people do want mod functionality.
Hi,
Found this thread while searching for solutions to a problem that seems fairly similar to comment 3. Awesome to see these new functions will eventually make it into my browser.
Firstly, was curious what very rough timeline I'd be looking at in terms of going "oh, \<browser> can do that now" :)? IOW, if I were to add a "revisit this" date to my mental calendar, would that be in say 2024, or halfway through 2023, or maybe even late 2022...? Etc. (I appreciate the general separation between spec and implementation (where dates become more concrete), hence the "rough". Perhaps there is related precedent in other similar areas to draw upon.)
Also, after considering the functionality being added, I wondered if these new features may not actually solve for my use-case as specified. Sometimes my slightly handwavy mental models of certain aspects of CSS leads me down dead ends, but I wonder if the way I interpreted things here may actually be interesting.
I happen to currently have a <canvas>
taking up maximal space, with a small legend to the side of it. I wanted the canvas to track the width of the page (with some JS handling internal resizing and repainting on resize) while allowing the legend to take up whatever space it naturally wanted to (size of text plus padding etc). I imagined being able to use these new functions to do something like round(100%, 1px)
... but then I realized, maybe that wouldn't quite work.
As currently specified.
I find myself hitting the walls of the percentage sizing model semi-frequently, and perhaps I've done that here (I should probably go read it...). I mention this use case in case I haven't gone completely off the deep end (in terms of "there's no semantically sensible spot in the usage model to slot this into"), because if it were reasonable to do this sort of thing (maybe where the percentage has a known size to work with in a parent somewhere?) it would make it possible to have my cake and eat it too: I'd be able to a) have the box model natively size my elements while b) locking my canvas' to a non-fractional width and thus eliminate antialiasing and blur.
Now, off to implement some sort of hacky workaround in JS involving leaving a dedicated width for the legend...
Firstly, was curious what very rough timeline I'd be looking at
It's been in the specs for quite a while; when it shows up in browsers is entirely up to browsers and we have no control over this (and in many cases, browsers don't know or choose not to announce timing information until something is actually ready to ship).
That said, I believe Safari and Chrome are actively implementing at the moment. Not sure about Firefox.
width: round(100%, 1px)
should work for ensuring that your element is an integer-pixel width, iiuc.
Thanks for replying and the info!
Announcing at implementation time makes a lot of sense, that would likely reduce overall noise. I'll be sure to keep an eye on Chrome, this is very neat.
Very cool to hear that this *will* actually support percentile values. That should make canvas alignment use cases a tad easier :heart:
(And I later realized I could just getBoundingClientRect()
on the legend. And if that ever started changing width I can just add a resize observer.)
Worth reading - CSS math:
https://css-tricks.com/using-absolute-value-sign-rounding-and-modulo-in-css-today
Graphic designers often specify a rectangular grid using a fixed module, i.e. a relative or absolute length that other measures are an integer multiple or subdivision of. In stylesheets, often several length units are used for different purposes, e.g. font size in
pt
, line height inem
, border width inpx
, margins inmm
, widths in%
, heights invh
and so on. To make them match up nicely and to assure the same results across different implementations, authors would need some method to influence or even control rounding behavior. Instead of classicround(value, precision)
,floor(value)
andceil(value)
functions found in many programming languages and spreadsheet applications for floating point numbers, I believe CSS users would best be served by a round to nearest multiple function, for values usually come with a unit. For the reasons given in #905 I would call itmod()
.