Skip to content

Commit

Permalink
Implement values with multiple units.
Browse files Browse the repository at this point in the history
  • Loading branch information
kaj committed Feb 27, 2021
1 parent 6e6304d commit fbdcd69
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 130 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ project adheres to

## Unreleased

### Breaking changes

* The unit of a `Numeric` is now a `UnitSet` rather than a `Unit`.

### Improvements

* Added a Contributing section to readme.
* Handle values with multiple units. PR #97.


## Release 0.18.0 - 2021-02-25

Expand Down
6 changes: 3 additions & 3 deletions src/parser/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ fn number(input: Span) -> IResult<Span, Value> {
unit,
)),
|(sign, num, unit)| {
Value::Numeric(Numeric {
value: if sign == Some(b"-") {
Value::Numeric(Numeric::new(
if sign == Some(b"-") {
// Only f64-based Number can represent negative zero.
if num.is_zero() {
(-0.0).into()
Expand All @@ -320,7 +320,7 @@ fn number(input: Span) -> IResult<Span, Value> {
num
},
unit,
})
))
},
)(input)
}
Expand Down
4 changes: 2 additions & 2 deletions src/sass/functions/color/hsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub fn to_rational_percent(v: &Value) -> Result<Rational, Error> {
match v {
Value::Null => Ok(Rational::zero()),
Value::Numeric(v, ..) => {
if v.unit == Unit::Percent || v.value > one() {
if v.unit.is_percent() || v.value > one() {
Ok(v.as_ratio()? / 100)
} else {
Ok(v.as_ratio()?)
Expand All @@ -174,7 +174,7 @@ pub fn to_rational2(v: &Value) -> Result<Rational, Error> {
match v {
Value::Null => Ok(Rational::zero()),
Value::Numeric(v, ..) => {
if v.unit == Unit::Percent {
if v.unit.is_percent() {
Ok(v.as_ratio()? / 100)
} else {
Ok(v.as_ratio()?)
Expand Down
4 changes: 2 additions & 2 deletions src/sass/functions/color/other.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{get_color, make_call, Error, FunctionMap};
use crate::css::Value;
use crate::value::{Hsla, Hwba, Rgba, Unit};
use crate::value::{Hsla, Hwba, Rgba};
use crate::Scope;
use num_rational::Rational;
use num_traits::{One, Signed, Zero};
Expand Down Expand Up @@ -259,7 +259,7 @@ fn to_rational(v: Value) -> Result<Rational, Error> {
fn to_rational_percent(v: Value) -> Result<Rational, Error> {
match v {
Value::Null => Ok(Rational::zero()),
Value::Numeric(v, _) if v.unit == Unit::Percent => {
Value::Numeric(v, _) if v.unit.is_percent() => {
Ok(v.as_ratio()? / 100)
}
Value::Numeric(v, ..) => {
Expand Down
6 changes: 3 additions & 3 deletions src/sass/functions/color/rgb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ fn int_value(v: Rational) -> Value {
fn to_int(v: &Value) -> Result<Rational, Error> {
match v {
Value::Numeric(v, ..) => {
if v.unit == Unit::Percent {
if v.unit.is_percent() {
Ok(v.as_ratio()? * 255 / 100)
} else {
v.as_ratio()
Expand All @@ -227,10 +227,10 @@ fn to_int(v: &Value) -> Result<Rational, Error> {

fn to_rational(v: &Value) -> Result<Rational, Error> {
match v {
Value::Numeric(num, _) if num.unit == Unit::Percent => {
Value::Numeric(num, _) if num.unit.is_percent() => {
Ok(num.as_ratio()? / 100)
}
Value::Numeric(num, _) if num.unit == Unit::None => num.as_ratio(),
Value::Numeric(num, _) if num.unit.is_none() => num.as_ratio(),
v => Err(Error::badarg("number", &v)),
}
}
Expand Down
13 changes: 5 additions & 8 deletions src/sass/functions/math.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{Error, FunctionMap, Scope};
use crate::css::Value;
use crate::value::{Number, Numeric, Quotes, Unit};
use crate::value::{Number, Numeric, Quotes, Unit, UnitSet};
use num_rational::Rational;
use rand::{thread_rng, Rng};
use std::cmp::Ordering;
Expand Down Expand Up @@ -62,7 +62,7 @@ pub fn create_module() -> Scope {
let unit = first.unit;
for v in rest {
let scaled = as_numeric(v)?
.as_unit(unit.clone())
.as_unitset(&unit)
.ok_or_else(|| Error::badarg(&unit.to_string(), v))?;
sum += f64::from(scaled).powi(2);
}
Expand Down Expand Up @@ -125,18 +125,15 @@ pub fn create_module() -> Scope {
def!(f, atan2(y, x), |s| {
let y = get_numeric(s, "y")?;
let x = get_numeric(s, "x")?;
let x = x.as_unit(y.unit).unwrap_or(x.value);
let x = x.as_unitset(&y.unit).unwrap_or(x.value);
Ok(deg_value(f64::from(y.value).atan2(f64::from(x))))
});

// - - - Unit Functions - - -
def!(f, compatible(number1, number2), |s| {
let u1 = get_numeric(s, "number1")?.unit;
let u2 = get_numeric(s, "number2")?.unit;
let ans = u1 == Unit::None
|| u2 == Unit::None
|| u1.dimension() == u2.dimension();
Ok(ans.into())
Ok(u1.is_compatible(&u2).into())
});
def!(f, is_unitless(number), |s| {
Ok((get_numeric(s, "number")?.is_no_unit()).into())
Expand Down Expand Up @@ -254,7 +251,7 @@ fn as_numeric(v: &Value) -> Result<Numeric, Error> {
}
}

fn number<T: Into<Number>>(v: T, unit: Unit) -> Value {
fn number(v: impl Into<Number>, unit: impl Into<UnitSet>) -> Value {
Numeric::new(v.into(), unit).into()
}

Expand Down
2 changes: 2 additions & 0 deletions src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod operator;
mod quotes;
mod range;
mod unit;
mod unitset;

pub use self::colors::{Color, Hsla, Hwba, Rgba};
pub use self::list_separator::ListSeparator;
Expand All @@ -15,5 +16,6 @@ pub use self::numeric::Numeric;
pub use self::operator::Operator;
pub use self::quotes::Quotes;
pub use self::unit::{Dimension, Unit};
pub use self::unitset::UnitSet;
pub use num_rational::Rational;
pub(crate) use range::{RangeError, ValueRange};
8 changes: 8 additions & 0 deletions src/value/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,14 @@ impl Number {
NumValue::Float(s) => Some(s.ceil() as isize),
}
}
/// Computes self^p.
pub fn powi(self, p: i32) -> Self {
match self.value {
NumValue::Rational(s) => s.pow(p).into(),
NumValue::BigRational(s) => s.pow(p).into(),
NumValue::Float(s) => s.powi(p).into(),
}
}

/// Get a reference to this `Value` bound to an output format.
pub fn format(&self, format: Format) -> Formatted<Self> {
Expand Down
57 changes: 46 additions & 11 deletions src/value/numeric.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use super::{Number, Rational, Unit};
use super::{Number, Rational, Unit, UnitSet};
use crate::output::{Format, Formatted};
use crate::Error;
use std::fmt::{self, Display};
use std::ops::Neg;
use std::ops::{Div, Mul, Neg};

/// A Numeric value is a [`Number`] with a [`Unit`] (which may be
/// Unit::None).
Expand All @@ -11,25 +11,25 @@ pub struct Numeric {
/// The number value of this numeric.
pub value: Number,
/// The unit of this numeric.
pub unit: Unit,
pub unit: UnitSet,
}

impl Numeric {
/// Create a new numeric value.
///
/// The value can be given as anything that can be converted into
/// a [`Number`], e.g. an [`isize`], a [`Rational`], or a [`f64`].
pub fn new(value: impl Into<Number>, unit: Unit) -> Numeric {
pub fn new<V: Into<Number>, U: Into<UnitSet>>(value: V, unit: U) -> Self {
Numeric {
value: value.into(),
unit,
unit: unit.into(),
}
}
/// Create a new numeric value with no unit.
pub fn scalar(value: impl Into<Number>) -> Numeric {
Numeric {
value: value.into(),
unit: Unit::None,
unit: UnitSet::scalar(),
}
}

Expand All @@ -43,7 +43,21 @@ impl Numeric {
/// assert_eq!(inch.as_unit(Unit::Deg), None);
/// ```
pub fn as_unit(&self, unit: Unit) -> Option<Number> {
self.unit.scale_to(&unit).map(|scale| &self.value * &scale)
self.unit
.scale_to_unit(&unit)
.map(|scale| &self.value * &scale)
}
/// Convert this numeric value to a given unit, if possible.
///
/// # Examples
/// ```
/// # use rsass::value::{Numeric, Unit};
/// let inch = Numeric::new(1, Unit::In);
/// assert_eq!(inch.as_unit(Unit::Mm).unwrap() * 5, 127.into());
/// assert_eq!(inch.as_unit(Unit::Deg), None);
/// ```
pub fn as_unitset(&self, unit: &UnitSet) -> Option<Number> {
self.unit.scale_to(unit).map(|scale| &self.value * &scale)
}

/// Convert this numeric value to a given unit, if possible.
Expand All @@ -69,7 +83,7 @@ impl Numeric {

/// Return true if this value has no unit.
pub fn is_no_unit(&self) -> bool {
self.unit == Unit::None
self.unit.is_none()
}

/// Get a reference to this `Value` bound to an output format.
Expand All @@ -91,7 +105,7 @@ impl PartialOrd for Numeric {
fn partial_cmp(&self, other: &Numeric) -> Option<std::cmp::Ordering> {
if self.unit == other.unit {
self.value.partial_cmp(&other.value)
} else if let Some(scaled) = other.as_unit(self.unit.clone()) {
} else if let Some(scaled) = other.as_unitset(&self.unit) {
self.value.partial_cmp(&scaled)
} else {
None
Expand All @@ -109,9 +123,30 @@ impl Neg for &Numeric {
}
}

impl Div for &Numeric {
type Output = Numeric;
fn div(self, rhs: Self) -> Self::Output {
Numeric {
value: &self.value / &rhs.value,
unit: &self.unit / &rhs.unit,
}
}
}
impl Mul for &Numeric {
type Output = Numeric;
fn mul(self, rhs: Self) -> Self::Output {
Numeric {
value: &self.value * &rhs.value,
unit: &self.unit * &rhs.unit,
}
}
}

impl<'a> Display for Formatted<'a, Numeric> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
self.value.value.format(self.format).fmt(out)?;
self.value.unit.fmt(out)
let mut unit = self.value.unit.clone();
let value = &self.value.value * &unit.simplify();
value.format(self.format).fmt(out)?;
unit.fmt(out)
}
}
Loading

0 comments on commit fbdcd69

Please sign in to comment.