use super::super::CheckedArg as _;
use super::css::required_arg;
use super::{known_dim, CallArgs, CallError, NumOrSpecial, ResolvedArgs};
use crate::css::{CssString, Value};
use crate::output::Format;
use crate::sass::functions::color::eval_inner;
use crate::sass::{ArgsError, FormalArgs};
use crate::value::{Number, Numeric, Quotes};
pub fn sass_round(s: &ResolvedArgs) -> Result<Value, CallError> {
let val: Numeric = s.get(name!(number))?;
Ok(Numeric::new(val.value.round(), val.unit).into())
}
pub fn css_round(s: &ResolvedArgs) -> Result<Value, CallError> {
let args = s.get_map(name!(kwargs), CallArgs::from_value)?;
if !args.named.is_empty() {
let fa = FormalArgs::new(vec![one_arg!(number)]);
return sass_round(&eval_inner(&name!(round), &fa, s, args)?);
}
if args.positional.len() > 3 {
return Err(CallError::msg(ArgsError::TooMany(
3,
args.positional.len(),
)));
}
let mut args = args.positional.into_iter();
let (strategy, number, step) = match (
Strategy::try_from(required_arg(args.next())?),
args.next(),
args.next(),
) {
(Ok(_), Some(num), None) if num.type_name() == "number" => {
return Err(CallError::msg(
"If strategy is not null, step is required.",
))
}
(Ok(s), Some(num), None) => (Some(s), num, None),
(Ok(s), Some(num), Some(step)) => (Some(s), num, Some(step)),
(Ok(_), None, _) => return Err(CallError::msg("xyzzy")),
(Err(v0), Some(num), Some(step)) => {
return if v0.type_name() == "variable" {
Ok(Value::call("round", [v0, num, step]))
} else {
Err(CallError::msg(format!(
"{} must be either nearest, up, down or to-zero.",
v0.format(Format::introspect()),
)))
};
}
(Err(v0), Some(step), None) => (None, v0, Some(step)),
(Err(v), None, _) => (None, v, None),
};
let number = NumOrSpecial::try_from(number).named(name!(number))?;
let step = step
.map(NumOrSpecial::in_calc)
.transpose()
.map_err(CallError::msg)?;
match (number, step) {
(NumOrSpecial::Num(num), Some(NumOrSpecial::Num(step))) => {
if let Some(step) = step.as_unitset(&num.unit) {
Ok(real_round(strategy.unwrap_or_default(), num, Some(step)))
} else if known_dim(&num)
.and_then(|dim1| known_dim(&step).map(|dim2| dim1 == dim2))
.unwrap_or(true)
{
Ok(fallback(strategy, num.into(), Some(step.into())))
} else {
Err(CallError::incompatible_values(num, step))
}
}
(NumOrSpecial::Num(num), None) => {
Ok(real_round(strategy.unwrap_or_default(), num, None))
}
(number, step) => Ok(fallback(strategy, number, step)),
}
}
fn real_round(
strategy: Strategy,
val: Numeric,
step: Option<Number>,
) -> Value {
let (val, unit) = (val.value, val.unit);
Value::from(Numeric::new(
if let Some(step) = step {
if step.is_finite() {
if (strategy == Strategy::ToZero) && step.is_negative() {
(&val / &step).abs().ceil() * step.abs() * val.signum()
} else {
strategy.apply(val / step.abs()) * step.abs()
}
} else if val.is_finite() {
if strategy == Strategy::Up && val > 0.into() {
Number::from(f64::INFINITY)
} else if strategy == Strategy::Down && val < 0.into() {
Number::from(f64::NEG_INFINITY)
} else {
val.signum() / Number::from(f64::INFINITY)
}
} else {
Number::from(f64::NAN)
}
} else {
strategy.apply(val)
},
unit,
))
}
fn fallback(
strategy: Option<Strategy>,
number: NumOrSpecial,
step: Option<NumOrSpecial>,
) -> Value {
match (strategy.map(Value::from), number, step) {
(Some(a1), a2, Some(a3)) => {
Value::call("round", [a1, a2.into(), a3.into()])
}
(Some(a1), a2, None) => Value::call("round", [a1, a2.into()]),
(None, a1, Some(a2)) => Value::call("round", [a1, a2]),
(None, a1, None) => Value::call("round", [a1]),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Strategy {
Nearest,
Up,
ToZero,
Down,
}
impl Strategy {
fn apply(&self, val: Number) -> Number {
match self {
Self::Nearest => val.round(),
Self::Up => val.ceil(),
Self::ToZero => val.trunc(),
Self::Down => val.floor(),
}
}
}
impl Default for Strategy {
fn default() -> Self {
Self::Nearest
}
}
impl TryFrom<CssString> for Strategy {
type Error = CssString;
fn try_from(value: CssString) -> Result<Self, Self::Error> {
if value.quotes() != Quotes::None {
return Err(value);
}
match value.value() {
"nearest" => Ok(Self::Nearest),
"up" => Ok(Self::Up),
"to-zero" | "to_zero" => Ok(Self::ToZero),
"down" => Ok(Self::Down),
_ => Err(value),
}
}
}
impl TryFrom<Value> for Strategy {
type Error = Value;
fn try_from(value: Value) -> Result<Self, Self::Error> {
match value {
Value::Literal(s) => s.try_into().map_err(Value::Literal),
other => Err(other),
}
}
}
impl From<Strategy> for Value {
fn from(value: Strategy) -> Self {
Self::from(match value {
Strategy::Nearest => "nearest",
Strategy::Up => "up",
Strategy::ToZero => "to-zero",
Strategy::Down => "down",
})
}
}