use alloc::string::String;
use super::decimal::{Decimal, RoundingMode};
use super::options::{DigitPlan, Grouping, RoundingPriority, RoundingType, Select, SignDisplay};
use super::{Number, Resolved, backend};
use crate::function::FnContext;
use crate::sink::{Sink, SubPartSink};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct NumberRequest<'r> {
pub value: &'r str,
pub style: NumberStyle<'r>,
pub neutral: bool,
pub digits: DigitOptions,
pub sign: SignDisplay,
pub grouping: Grouping,
pub ordinal: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub struct DigitOptions {
pub minimum_integer: u8,
pub fraction: Option<(u8, u8)>,
pub significant: Option<(u8, u8)>,
pub priority: RoundingPriority,
pub increment: u16,
pub mode: RoundingMode,
pub strip_if_integer: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum NumberStyle<'a> {
Decimal,
Percent,
Currency {
code: &'a str,
display: CurrencyDisplay,
accounting: bool,
own_digits: bool,
},
Unit {
unit: &'a str,
display: UnitDisplay,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum CurrencyDisplay {
Symbol,
NarrowSymbol,
Name,
Code,
Never,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum UnitDisplay {
Short,
Narrow,
Long,
}
pub enum NumberOut<'o> {
Text(&'o mut dyn Sink),
Parts(&'o mut dyn SubPartSink),
}
impl core::fmt::Debug for NumberOut<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
NumberOut::Text(_) => "Text(..)",
NumberOut::Parts(_) => "Parts(..)",
})
}
}
pub(crate) struct Text {
buf: [u8; 64],
len: usize,
heap: Option<String>,
}
impl Text {
pub(crate) const fn new() -> Text {
Text {
buf: [0; 64],
len: 0,
heap: None,
}
}
pub(crate) fn as_str(&self) -> &str {
match &self.heap {
Some(s) => s,
None => core::str::from_utf8(self.buf.get(..self.len).unwrap_or(&[])).unwrap_or(""),
}
}
pub(crate) fn plain(d: &Decimal) -> Text {
let mut t = Text::new();
d.write_plain(&mut t);
t
}
}
impl Sink for Text {
fn push_str(&mut self, s: &str) {
if let Some(h) = &mut self.heap {
Sink::push_str(h, s);
return;
}
let end = self.len + s.len();
if end <= self.buf.len() {
for (d, &b) in self.buf.iter_mut().skip(self.len).zip(s.as_bytes()) {
*d = b;
}
self.len = end;
return;
}
let mut h = String::new();
Sink::push_str(&mut h, self.as_str());
Sink::push_str(&mut h, s);
self.heap = Some(h);
}
}
pub(crate) const INTL_DIGITS: u8 = 21;
impl DigitPlan {
fn options(&self) -> DigitOptions {
let (inc, k) = self.increment;
let mut increment = u16::from(inc.value());
for _ in 0..k {
increment = increment.saturating_mul(10);
}
let clamp = |n: u8| n.clamp(1, INTL_DIGITS);
DigitOptions {
minimum_integer: clamp(self.min_int),
fraction: (self.ty != RoundingType::Significant)
.then_some((self.min_frac, self.max_frac)),
significant: (self.ty != RoundingType::Fraction)
.then_some((clamp(self.min_sig), clamp(self.max_sig))),
priority: match self.ty {
RoundingType::More => RoundingPriority::MorePrecision,
RoundingType::Less => RoundingPriority::LessPrecision,
RoundingType::Fraction | RoundingType::Significant => RoundingPriority::Auto,
},
increment,
mode: self.mode,
strip_if_integer: self.strip_if_integer,
}
}
}
fn exact_options(d: &Decimal) -> DigitOptions {
let f = u8::try_from(-i32::from(d.low().min(0)))
.unwrap_or(100)
.min(100);
DigitOptions {
minimum_integer: 1,
fraction: Some((f, f)),
significant: None,
priority: RoundingPriority::Auto,
increment: 1,
mode: RoundingMode::HalfExpand,
strip_if_integer: false,
}
}
impl Resolved {
pub(super) fn request<'r>(
&self,
value: &'r str,
style: NumberStyle<'r>,
neutral: bool,
) -> NumberRequest<'r> {
let mut digits = backend::plan(self).options();
if let NumberStyle::Currency {
own_digits: true, ..
} = style
{
digits.fraction = None;
}
NumberRequest {
value,
style,
neutral,
digits,
sign: self.opts.sign_display.unwrap_or(SignDisplay::Auto),
grouping: self.opts.use_grouping.unwrap_or(Grouping::Auto),
ordinal: self.opts.select == Some(Select::Ordinal),
}
}
}
impl Number {
#[doc(hidden)]
pub fn format_by_host(
&self,
cx: &FnContext<'_>,
style: NumberStyle<'_>,
neutral: bool,
out: NumberOut<'_>,
) -> bool {
let text = Text::plain(&self.value);
let request = match &self.resolved {
Some(r) => r.request(text.as_str(), style, neutral),
None => NumberRequest {
value: text.as_str(),
style,
neutral,
digits: exact_options(&self.value),
sign: SignDisplay::Auto,
grouping: Grouping::Auto,
ordinal: false,
},
};
cx.host()
.numbers()
.is_some_and(|f| f.format(cx.locale(), &request, out))
}
}