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

Better unit manipulation functions #740

Closed
lunelson opened this issue May 8, 2013 · 14 comments
Closed

Better unit manipulation functions #740

lunelson opened this issue May 8, 2013 · 14 comments

Comments

@lunelson
Copy link

lunelson commented May 8, 2013

I'm encountering some weird bugs with various methods of unit-manipulation that I've tried to do in pure Sass, so I wrote them in SassScript instead.

This is just a starting point; and they lack input error checking but they are pretty handy.

IMHO some version of this should be built in to core Sass... in the meantime I run them as an ad-hoc compass extension

module Sass::Script::Functions

  # return a number without its unit
  def remove_unit(number)
    assert_type number, :Number
    Sass::Script::Number.new(number.value)
  end
  declare :remove_unit, [:number]

  # return 1 * the unit of the input number
  def one_unit(number)
    assert_type number, :Number
    Sass::Script::Number.new(1, ["#{number.unit_str}"])
  end
  declare :one_unit, [:number]

  # force the unit of a given number
  def assert_unit(number, unit)
    assert_type number, :Number
    assert_type unit, :String
    Sass::Script::Number.new(number.value, ["#{unit}"])
  end
  declare :assert_unit, [:number]

end

usage examples:

//=> input
.unit-test {
    line-height: remove_unit(25px);
    font-size: one_unit(100%);
    height: assert_unit(5,rem);
}
//=> output
.extest {
  line-height: 25;
  font-size: 1%;
  height: 5rem; }
@nex3
Copy link
Contributor

nex3 commented May 10, 2013

I'd accept a pull request against master to add these, although there are a number of changes that would have to be done:

  • I'm not a fan of the destructive implication of remove-unit. without-unit would be better.
  • one-unit is redundant with assert-unit and the existing unit function.
  • assert-unit would be better named with-unit.
  • Directly passing unit_str will break with complex units (e.g. px*px).
  • Documentation and tests would of course be necessary.

@lunelson
Copy link
Author

I'd be glad to give this a shot. If I leave the one_unit() function out (I agree it's redundant with assert_unit()) I guess that avoids the unit_str problem you noted?

Beyond that I would just need to know where to look in the current codebase for how to write tests and documentation...

@nex3
Copy link
Contributor

nex3 commented May 13, 2013

Tests go in test/sass/functions_test, documentation should be both attached to the functions themselves and in the doc comment at the top of lib/sass/script/functions.

@chriseppstein
Copy link

I have to say, I'm not a huge fan of these functions. I think they encourage sloppy, inexpressive code and surprising output. I've yet to encounter a use case where the mathematical expression to convert the unit wasn't safer and simple to understand. Please provide use cases for why you think these are necessary.

@chriseppstein
Copy link

For reference, here is how compass lets a user convert a number to have different units: https://github.com/chriseppstein/compass/blob/master/frameworks/compass/stylesheets/compass/typography/_units.scss#L12-L104

We make sure that the numbers are converted correctly according the the defined ratios or details about the conversion's style context. I'm really worried we'll be seeing a lot of code that simply extracts the unit of one number and smashes it into another when a conversion is actually required.

@nex3
Copy link
Contributor

nex3 commented May 21, 2013

@chriseppstein I'm certainly sympathetic to those concerns. At the same time, we've been moving in the direction of adding more introspective functions, and right now there really isn't a way to do what these functions are doing. But maybe you're right and there isn't a reason to do what these functions are doing in the first place.

I agree that we should see some use cases before we put these in.

@cimmanon
Copy link

I don't have a use for anything other than the one to remove the unit.

// @font-size:
//      the size of the current element (eg. `1.5em`)
// @desired-size:
//      the desired output size (eg. `1em`)
// @base-size:
//      the size of the parent element if you're trying to scale it back to the baseline size (eg. `1.5em` or `.8em`)
//      otherwise use the default of `1em` to signify no additional scaling
@function scale-em($font-size, $desired-size, $parent-size: 1em) {
    @return if($desired-size == 0, 0, strip-unit($desired-size) / strip-unit($font-size) * $parent-size);
}

.foo {
    font-size: 1.5em;

    .bar {
        font-size: scale-em(1.5em, 1em);
    }
}

Output:

.foo {
  font-size: 1.5em;
}

.foo .bar {
  font-size: 0.66667em;
}

The Compass convert-length() function insists that I need to provide pixel values for the $from-context and $to-context, which is more work for my particular use case. Also, I think foo(1, em) looks less elegant than foo(1em).

Most people seem to be avoiding working with ems by using rem with px fallback. If I'm using rem (which I do on occasion to avoid rounding inconsistencies), I want an em fallback.

@lunelson
Copy link
Author

My use-case is essentially that I prefer to write complex mixins or functions without having to worry about units. I check the units (or alternately, the ranges) of incoming arguments to know how to handle them; but then I prefer to strip the unit—so I don't have to handle the presence or non-presence of it in internal calculations—and just assert the required output unit on the last line, if one is needed. Stripping the unit, is more common than asserting it but I do use both.

One example is with percentage as an input. The input value might be in the range 0-100, with or without a percentage unit, or it might be in the range 0-1. It's easier to do a simple check and then strip the unit for the purpose of internal calculation, and write a function/mixin that can accept united or non-united values without failing.

I also use these in typographic sizing, where I have various units and fallback units as well as no-units (line-height) going on. Having the units hanging around just makes things too complicated, it's easier to set them on output. Obviously anybody who uses these functions needs to know what they are doing and needs to test their code.

@lunelson
Copy link
Author

Actually strip-unit() is what I call it in my own code too. I agreed with @nex3's point that one-unit() wasn't necessary and I'm amenable to calling the other two with-unit() and without-unit() but infact I call them strip-unit() and assert-unit().

As for the return statements you wrote above, it's precisely because of buggy results in using $number / ($number * 0 + 1) as a means to strip units, that I chose to write them in SassScript, which has proven to be much more robust.

@nex3
Copy link
Contributor

nex3 commented May 25, 2013

@cimmanon the strip-unit calls in that code are superfluous. $font-size and $desired-size both have em as their unit, so $font-size / $desired-size has no unit anyway.

@lunelson writing functions that don't think about units sounds like an anti-pattern.

The input value might be in the range 0-100, with or without a percentage unit, or it might be in the range 0-1.

This sounds very confusing. How do I know whether fn(1) is going to treat 1 as 1% or 1.0? This is why units exist: to make it clear what you're specifying.

If the unit math doesn't work out in your functions, that seems a logic smell rather than an indication that Sass needs an easier way to add/remove units.

@robwierzbowski 1#{unit($number)} doesn't work how you want it to. It returns a string, not a number.

Summary

I don't find these use cases very compelling; I'm leaning toward's @chriseppstein's position here, that these functions would encourage more sloppy code than they do add expressive power.

@robwierzbowski
Copy link

@nex3, Sorry, you're right. Should be something like $number * 0 + 1, but the point is moot.

@chriseppstein
Copy link

@lunelson thank you for the patch, but I think the cons outweigh the pros here.

@jakob-e
Copy link

jakob-e commented May 28, 2013

If any help - when converting units* I use these home cookings (SCSS)
(*Better than remove units - that doesn't tell us what we get back :)

// Convert to number 
@function number($val){
    $u:unit($val);
    $u:if($u==px,1px,
       if($u==em,1em,
       if($u==rem,1rem,
       if($u=='%',1%,
       if($u==pt,1pt,
       if($u==pc,1pc,
       if($u==in,1in,
       if($u==mm,1mm,
       if($u==cm,1cm,
       1)))))))));
    @return $val/$u;
}

// Example:
.to_number { 
    px:number(1px); 
    em:number(1em);
    rem:number(1rem);
    pct:number(1%);
    pt:number(1pt);
    pc:number(1pc);
    in:number(1in);
    mm:number(1mm);
    cm:number(1cm);
    num:number(1);
}
// Output: 
.to_number {
  px: 1;
  em: 1;
  rem: 1;
  pct: 1;
  pt: 1;
  pc: 1;
  in: 1;
  mm: 1;
  cm: 1;
  num: 1;
}

With the number converter you can now build a base unit converter.

I build my functions on a pixel conversion table with a little help from: http://www.translatorscafe.com/cafe/units-converter/typography/calculator/pixel-(X)-to-centimeter-[cm]/
Thank you guys :)

Note! I use html { font-size:100%; }

// Unit Conversion Table 
// 1px = 0.0625em;
// 1px = 0.0625rem;
// 1px = 0.0625pc;
// 1px = 0.75pt;  
// 1px = 0.010416667in; 
// 1px = 0.264583333mm;
// 1px = 0.026458333cm;

// Convert to px 
@function px($val){
    $u:unit($val);
    $u:if($u==px,1,
       if($u==em,0.0625,
       if($u==rem,0.0625,   
       if($u==pt,0.75, 
       if($u==pc,0.0625,
       if($u==in,0.010416667,
       if($u==mm,0.264583333, 
       if($u==cm,0.026458333, 
       1))))))));
    @return number($val) / $u * 1px;
}

// Example:
.to_px { 
    px:px(1px); 
    em:px(1em);
    rem:px(1rem);
    pt:px(1pt);
    pc:px(1pc);
    in:px(1in);
    mm:px(1mm);
    cm:px(1cm);
    num:px(1);
}

// Output:
.to_px {
  px: 1px;
  em: 16px;
  rem: 16px;
  pt: 1.33333px;
  pc: 16px;
  in: 96.0px;
  mm: 3.77953px;
  cm: 37.79528px;
  num: 1px;
}

With the base unit converter you can now do the rest - like:

@function em($val) { @return if(unitless($val),$val * 1em,number(px($val)) * 0.0625em); }
@function rem($val){ @return if(unitless($val),$val * 1rem,number(px($val)) * 0.0625rem); }
@function pt($val) { @return if(unitless($val),$val * 1pt,number(px($val)) * 0.75pt); }
@function pc($val) { @return if(unitless($val),$val * 1pc,number(px($val)) * 0.0625pc); }
@function in($val) { @return if(unitless($val),$val * 1in,number(px($val)) * 0.010416667in); }
@function mm($val) { @return if(unitless($val),$val * 1mm,number(px($val)) * 0.264583333mm); }
@function cm($val) { @return if(unitless($val),$val * 1cm,number(px($val)) * 0.026458333cm); }


// Example:
.px_to_em_to_cm_to_in_to_back { 
  px:px(in(cm(em(px(16))))); 
}


// Output: 
.px_to_em_to_cm_to_in_to_back {
  px: 16px;
}

If you want to extend the em converter to handle parents - you could do something like this:

@function em($args...){
    $val:nth($args,1);
    $em:if(unitless($val),$val * 1em,number(px($val)) * 0.0625em);
    @for $i from 2 through length($args){$em:$em / number(em(nth($args,$i)))}
    @return $em;
}   

// Example:
.one_in_half_in_four_in_two { 
  em:em(1, 0.5em, 4em, em(2));
}

// Output:
.one_in_half_in_four_in_two {
  em: 0.25em;
}

Have a wonderful day :)

Best,
Jakob E

@amclin
Copy link

amclin commented Apr 21, 2015

@chriseppstein a valid use case for stripping units is in doing math to convert to or from vw/vh units (for example calculating suitable px or em values for fallback in browsers that lack vw/vh support). Since there is no uniform way of handling this conversion it must be done on a case-by-case basis, and requires stripping units.

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

No branches or pull requests

7 participants