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] Add round()/floor()/ceil() functions #2513

Closed
Crissov opened this issue Apr 6, 2018 · 32 comments
Closed

[css-values] Add round()/floor()/ceil() functions #2513

Crissov opened this issue Apr 6, 2018 · 32 comments

Comments

@Crissov
Copy link
Contributor

Crissov commented Apr 6, 2018

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 in em, border width in px, margins in mm, widths in %, heights in vh 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 classic round(value, precision), floor(value) and ceil(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 it mod().

mod(101px, 1pt/* = 76pt ≈ 101.33px > 101px */
mod(100px, 1pt) /* = 75pt = 100px */
mod( 99px, 1pt/* = 74pt ≈  98.67px < 99px */
mod( 10px, 1pt/* =  8pt ≈  10.67px > 10px */
mod(  1px, 1pt/* =  1pt ≈   1.33px > 1px */
@Loirooriol
Copy link
Contributor

I think mround(value, multiple) would be a more intuitive name.

Alternatively, with round one could use calc(multiple * round(value/multiple))

@jonjohnjohnson
Copy link

jonjohnjohnson commented Apr 25, 2018

Though I see the more full featured request which spawned this issue was closed for lack of use cases (#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.

Pretty sure that solving rounding would save a lot of headaches

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.

@Crissov
Copy link
Contributor Author

Crissov commented Apr 26, 2018

@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.)

@tabatkins
Copy link
Member

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.

@tabatkins tabatkins changed the title [css-values] Round to design grid module [css-values] Add round()/floor()/ceil() functions Dec 13, 2019
@astearns
Copy link
Member

Should/could CSS have a default scale for this purpose? Or can these functions locally define a default for an omitted precision parameter?

@Crissov
Copy link
Contributor Author

Crissov commented Dec 13, 2019

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.

@Loirooriol
Copy link
Contributor

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)

@tabatkins
Copy link
Member

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.

@Crissov
Copy link
Contributor Author

Crissov commented Dec 14, 2019

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:

@AmeliaBR
Copy link
Contributor

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 */

@Loirooriol
Copy link
Contributor

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?

@tabatkins
Copy link
Member

@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.

@tabatkins
Copy link
Member

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.

@jimmyfrasche
Copy link

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.

@tabatkins
Copy link
Member

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.

@Crissov
Copy link
Contributor Author

Crissov commented Jan 17, 2020

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.

@css-meeting-bot
Copy link
Member

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 full IRC log of that discussion <bkardell_> topic: VAlues and units round()/floor()/ceil()/mod() (tab)
<bkardell_> TabAtkins: As we knew would probably happen, as we started suggesting math functions, people wanted more math functions
<bkardell_> TabAtkins: problems in the past the prevented these, we have solved those - so this issue is specifically about adding round and mod functions - they are all similar under the hood
<astearns> github: https://github.com//issues/2513
<bkardell_> TabAtkins: the big question for me is whether we want to add these as 4 function names or 1 with a mode?
<bkardell_> chrisl: which will people be most familliar with
<bkardell_> TabAtkins: another concern is spelling, for me ceil or ciel is confusing to me - I suspect I am not the only one... my suggestion is we go with round function
<bkardell_> TabAtkins: Any ideas, comments suggestions?
<fantasai> TabAtkins writes on the blackboard: round(keyword? calc, calc, ...)
<fantasai> s/, ...//
<bkardell_> TabAtkins: round(keyword?, calc, calc) -- the second being precision you want
<bkardell_> leaverou: is there any precedent of having a single function like this in any language?
<bkardell_> TabAtkins: we have several functions with optional keywords in css
<bkardell_> leaverou: fair enough
<chris> rrsagent, here
<RRSAgent> See https://www.w3.org/2020/01/22-css-irc#T09-34-34
<bkardell_> fremy: when does round function happen
<bkardell_> TabAtkins: same time as all of the math functions
<bkardell_> jensimmons: it seems like we asked this on twitter and we are now seeming to go against their express intent in this, admittedly non-scientific poll
<chris> alias sky ceiling
<bkardell_> fantasai: people who come from a math/programming background get 'floor' and 'ceiling' but as someone coming new to programming you won't know that
<bkardell_> myles: it's a math term
<bkardell_> fantasai: i didn't learn it, I don't think a lot of us did. For discoverability of people who are not already familliar with these terms it is a better model
<chris> round-up | round-down | round-truncate
<Rossen__> q?
<bkardell_> leaverou: don't forget that there is limited value in asking people because they will always trend toward whatever they already know
<bkardell_> rachelandrew: people will trend toward the simplest things
<bkardell_> dbaron: is this proposal written in the issue? I didn't find it in these comments?
<bkardell_> fremy: one of them was from amelia, I thin
<bkardell_> s/thin/think
<fantasai> s/better model/better model, because all four functions that do almost the same thing with slight variations are under the same feature. With the 4 separate functions, you have to now that they exist, and it's harder to find them unless you already know their names/
<bkardell_> jensimmons: consistency in css is a strong argument
<fantasai> i/jensimmons/rossen?: Could bake the keywords into the function name, like round-up(), round-down(), etc./
<bkardell_> Rossen__: it sounds like we all are agreeing that one function with an optional keyword...
<jensimmons> (uh, that’s not what I said.) I said — it makes sense to me what fantasai said, that it is basically one function / one feature — with several different ways to do it.
<bkardell_> florian: I was just looking and ruby has this pattern actually, the one we're suggesting
<dbaron> you can get to 8 functions with up|down|towards-zero|away-from-zero|nearest-half-goes-up|nearest-half-goes-down|nearest-half-goes-away-from-zero|nearest-half-goes-towards-zero
<florian> https://ruby-doc.org/core-2.5.3/Float.html#method-i-round
<bkardell_> Rossen__: prior art, agreement in the room, any objections
<fantasai> i/jensimmons/fantasai: There's no typing benefit to doing that, should just follow the CSS pattern we have in gradients, cross-fade(), etc. to put the keyword in the parens/
<dbaron> and I think the comment pointed to was https://github.com//issues/2513#issuecomment-565736728
<bkardell_> Rossen__: ok resolved
<bkardell_> RESOLVED: Adopt a round function with keywords detailing which behavior
<bkardell_> TabAtkins: mod() same deal - it's similar to mod() anywhere else, but I suggest we don't match javascript here
<bkardell_> myles: there is value in matching js here
<dbaron> Tab is suggesting that the sign of the result should match the sign of the second argument, not the first
<bkardell_> TabAtkins: from what I can tell, most of the time people want the sign of the modulus, not the sign of the first arg
<bkardell_> myles: I don't have a strong opinion, I think there is a strong case to match JavaScript though
<Rossen__> q?
<bkardell_> TabAtkins: I feel mathematical mod is more sensible, how you probably learned in school - not as done in js as adopted from Java as adopted from C
<bkardell_> jensimmons: I think matching JavaScript is valuable
<astearns> +1 to jensimmons
<bkardell_> Rossen__: breaking the relationship to JavaScript seem to be a bad default - people use JavaScript to create CSS
<fantasai> fantasai: what are the use cases for modding negative numbers?
<bkardell_> dbaron: if I am imagining some step pattern or some kind of thing you would create here - it does seem like what TabAtkins is saying is going to be more natural
<fantasai> +1
<bkardell_> TabAtkins: there seems to be good argument both ways
<bkardell_> fantasai: I would lean toward dbaron argument
<fantasai> fantasai: people who use JS are going to be more comfortable mucking about with mod functions to get the behavior they want
<dbaron> I was arguing that if you're using mod() to generate step patterns or something like that, if we use the JS mod(), you'll have to be careful to avoid inputting negative numbers (or, as Tab said, using multiple-mod() workarounds for that).
<fantasai> s/both ways/both ways; and you can use mod + add + mod calc pattern to switch behaviors if needed/
<bkardell_> bkardell_: Lots of preprocessors have functions like this -- which do they have, does it matter?
<bkardell_> TabAtkins: they don't seem to have an example
<bkardell_> TabAtkins: they don't seem to have an example in sass
<fantasai> fantasai: but this way the people who are just trying to get their CSS to work don't have to try so hard
<bkardell_> TabAtkins: just looking at the wikipedia page, you can see this problem demonstrated
<bkardell_> TabAtkins: it's very easy to just accidentally go negative and then you will have broken code, whereas the way I am suggesting here you have to be more intentional about it
<bkardell_> TabAtkins: good argument in general that matching js is good - but we have also agreed that it is maybe less intuitive
<bkardell_> TabAtkins: what do we want to do?
<bkardell_> Rossen__: straw poll for the initial thing, we can always change our minds
<bkardell_> Rossen__: 1) align with js 2) align with 'math'
<leaverou2> 2
<fremy> 2
<TabAtkins> 2
<heycam> 1
<bkardell_> 1
<jfkthame> 1
<iank_> 1
<Rossen__> 1
<stantonm> 1
<rachelandrew> 1
<emilio> 1
<florian> 2
<astearns> 1
<cbiesinger> 2
<jensimmons> on behalf of dbaron — 2
<bkardell_> myles: 1
<bkardell_> tess: 1
<bkardell_> bkardell_: 1
<bkardell_> TabAtkins: 11 to 10, I think we have to leave this as an open issue
<bkardell_> Rossen__: any objections to adding the mod() function with an open issue about....what it actually does?
<fantasai> i/TabAtkins/[various people have lost connectivity]
<fantasai> s/connectivity/connectivity; Tab runs an offline poll/
<jensimmons> the vote in the room was 12 for Option 1 (align with JS) and 11 for Option 2 (align with math)
<bkardell_> RESOLVED: add the mod function with an open issue about behavior
<fantasai> It was 11 against 11 of the people in the room; myles claimed to vote for Tess, but she wasn't here for the oral arguments :p

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed update on mod() function.

The full IRC log of that discussion <dbaron> Topic: update on mod() function
<TabAtkins> https://twitter.com/tabatkins/status/1219936010961915905
<emilio> ScribeNick: emilio
<astearns> github: https://github.com//issues/2513
<emilio> TabAtkins: so the poll I started yesterday about mod has ended
<emilio> ... had some fair results, and the contradictory results I expected
<emilio> ... so 3/2 in favor of JS, 9/1 in favor of math, depending on how you ask
<emilio> ... so the conclusion is that most people just write buggy code and they think / want math semantics
<fantasai> /JS/JS semantics if you ask directly/
<emilio> ... there's also a lot of discussion in the replies about use-cases
<jensimmons> link to poll??
<fantasai> s/favor of math/favor of math if you ask them about a basic computation/
<emilio> ... and it seems math is a better suit to fix those use cases
<emilio> jfkthame: would people be better off with explicit is-odd / even functions
<fantasai> https://twitter.com/tabatkins/status/1219936010961915905
<fantasai> https://twitter.com/tabatkins/status/1219936010961915905
<emilio> TabAtkins: they sometimes use it as a proxy for odd / even, but it's not general of course
<TabAtkins> https://twitter.com/tabatkins/status/1219939184682717184
<emilio> TabAtkins: so no decision for now yet unless the room is convinced, but worth thinking about it and we can peek this up at a later call
<emilio> TabAtkins: how does the room feel? Did anyone change their mind?
<emilio> myles_: I guess there's a third option which is not defining what negatives does
<emilio> TabAtkins: that's what C++ does and that's evil
<emilio> myles_: I didn't mean to return unicorns but just explicitly return 0 or something
<emilio> TabAtkins: it seems negatives could be common, I wouldn't want that
<emilio> Rossen: seems not many opinions have changed so let's move on

@tabatkins
Copy link
Member

Added round() in 90ff717. mod() is waiting on final resolution on naming; I hope to resolve that today.

@css-meeting-bot
Copy link
Member

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
The full IRC log of that discussion <fremy> Topic: mod() mode
<astearns> github: https://github.com//issues/2513
<TabAtkins> https://en.wikipedia.org/wiki/Modulo_operation#In_programming_languages
<fremy> TabAtkins: I was thinking about this, and looking at the wikipedia article...
<fremy> TabAtkins: there is a lot of divergence
<fremy> TabAtkins: but there is one constant
<fremy> TabAtkins: if a language has a pair, mod works like Math and % works like JS
<fantasai> s/%/rem/
<fremy> TabAtkins: in particular, <<<<>>>> language does this, because they came to the same conclusion
<heycam> q+
<fremy> TabAtkins: so my proposal, is to add the two functions
<fantasai> s/<<<>>>>/Web Assembly/
<fremy> leaverou: why are we adding functions and not an operator
<fremy> TabAtkins: more complex
<leaverou> s/why are we adding functions and not an operator/why are these functions and not operators?/
<fremy> TabAtkins: and also it's not an operator in all languages anyway
<fantasai> I support Tab's proposal fwiw
<fremy> myles: why dont' we want mod to be like js?
<florian> +1
<florian> s/+1/+1 to tab's proposal/
<fremy> TabAtkins: javascript doesn't have a function, just an operator that works like rem
<fremy> myles: but then we make CSS more powerful than JS, while JS was intended to be math-complete while CSS is not
<fremy> hober: I think the point of this exercise was to reduce differences in differences between the platform languages
<fremy> hober: I'm confused why we want to introduce inconsistencies
<RossenF2F> q?
<fremy> hober: It might be reasonable to ask somebody from TC39 to consider adding the other function, and mimick their response
<fremy> TabAtkins: I don't believe it's correct to say that we want to minimize the difference between the two languages
<fremy> TabAtkins: our goal is to give designers the functions they need to get the layouts they want
<fremy> TabAtkins: which is why we didn't add the hyperbolic functions because there are no known use case
<fremy> TabAtkins: and to reply to the "more powerful question", we already do that
<fremy> TabAtkins: for instance, we have a more complex round function, and that is useful because rouding is important to us
<fremy> TabAtkins: so I don't want to say we should not innovate beyond JS
<fremy> TabAtkins: but we should only do it if there's an use case
<astearns> ack heycam
<astearns> Zakim, close queue
<Zakim> ok, astearns, the speaker queue is closed
<fremy> heycam: If we really wanted to match JS, we would do an operator, not a mod function in the first place
<fremy> heycam: but I was on the queue to say something else
<fremy> heycam: it's confusing to have an unit called rem, and a function called rem
<fremy> heycam: I would like to avoid it
<fremy> myles: an author might think it gives you the size of a rem
<fremy> TabAtkins: it would not work, and authors would realize that
<astearns> ack fantasai
<Zakim> fantasai, you wanted to point out that JS doesn't have a mod function, it has a % operator, so either way we have to pick a name, picking rem() as that name is perfectly fine
<fremy> fantasai: also, I want to point out that log doesn't do console.log
<fremy> fantasai: also, I agree, % is not a function in JS, not a function
<myles> q?
<fremy> fantasai: so, if we add a mod() function we don't have to match JS, we can do something useful
<fremy> myles: log is a very different example, because we just cannot console.log in JS
<fremy> myles: but here we are doing the same thing
<fremy> TabAtkins: we have functions that can absorb css timing resolutions from houdini
<fremy> TabAtkins: the paint api when it gets called gives you timing information
<fremy> astearns: I don't think this is gonna get narrowed down today
<fremy> TabAtkins: but, we want to check if we can ship this
<fremy> TabAtkins: so can we strawpoll or record objections?
<fremy> <debate on the options between people>
<fremy> RossenF2F: the new option is that we have both mod and rem
<fremy> florian: I think we should just have a decision yes or no on the adoption of this approval
<fremy> astearns: yeah, let's do this
<fremy> hober: I am worried about the form of this strawpol
<fremy> hober: because we are not agreeing on an alternative
<fremy> astearns: yes, but if we say no, we don't do anything and defer for another day
<TabAtkins> 1. Resolve to add mod() (math behavior) and rem() (JS behavior)
<TabAtkins> 2. Continue thinking about the desired mod-ish behavior in the issue, resolve sometime later.
<fantasai> 1
<florian> 1
<TabAtkins> 1
<faceless> 1
<leaverou> 1
<hober> 2
<dbaron> 1
<RossenF2F> 1
<fremy> 1
<tantek> 1 because YOLO
<stantonm> 1
<rachelandrew> 1
<astearns> abstain
<jfkthame> 1
<heycam> 1
<fremy> 15/22 said 1
<RossenF2F> mod(15/22)
<fremy> astearns: proposed resolution is thus to add both mod() and rem()
<fremy> astearns: does anybody object?
<TabAtkins> It's 13 for option 1, 1 for option 2
<fremy> RESOLVED: the mod() and rem() functions are to be added to CSS, one with math behavor, the other with JS behavior
<TabAtkins> ScribeNick: TabAtkins
<leaverou> Chris can't vote due to lack of connectivity, but he votes 1

@AmeliaBR
Copy link
Contributor

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>)

@tabatkins
Copy link
Member

tabatkins commented Jan 24, 2020

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.

@tabatkins
Copy link
Member

(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.)

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jan 24, 2020

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. 8%-3 gives me +2 in my Chrome and Firefox consoles. Nevermind. JS % 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.

@bradkemper
Copy link
Contributor

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?

@Crissov
Copy link
Contributor Author

Crissov commented Jan 25, 2020

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.

@tabatkins
Copy link
Member

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.

@exikyut
Copy link

exikyut commented Dec 14, 2021

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...

@tabatkins
Copy link
Member

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.

@exikyut
Copy link

exikyut commented Dec 15, 2021

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 ❤️

(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.)

@yairEO
Copy link

yairEO commented Feb 3, 2022

@Crissov
Copy link
Contributor Author

Crissov commented Dec 10, 2023

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

No branches or pull requests