UPDATE: Dump of initial files
This commit is contained in:
17
base/src/formatter/dates.rs
Normal file
17
base/src/formatter/dates.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use chrono::Datelike;
|
||||
use chrono::Duration;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::constants::EXCEL_DATE_BASE;
|
||||
|
||||
pub fn from_excel_date(days: i64) -> NaiveDate {
|
||||
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
|
||||
dt + Duration::days(days - 2)
|
||||
}
|
||||
|
||||
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
||||
match NaiveDate::from_ymd_opt(year, month, day) {
|
||||
Some(native_date) => Ok(native_date.num_days_from_ce() - EXCEL_DATE_BASE),
|
||||
None => Err("Out of range parameters for date".to_string()),
|
||||
}
|
||||
}
|
||||
763
base/src/formatter/format.rs
Normal file
763
base/src/formatter/format.rs
Normal file
@@ -0,0 +1,763 @@
|
||||
use chrono::Datelike;
|
||||
|
||||
use crate::{locale::Locale, number_format::to_precision};
|
||||
|
||||
use super::{
|
||||
dates::{date_to_serial_number, from_excel_date},
|
||||
parser::{ParsePart, Parser, TextToken},
|
||||
};
|
||||
|
||||
pub struct Formatted {
|
||||
pub color: Option<i32>,
|
||||
pub text: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Returns the vector of chars of the fractional part of a *positive* number:
|
||||
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
|
||||
fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
||||
let b = format!("{:.1$}", value.fract(), precision as usize)
|
||||
.chars()
|
||||
.collect::<Vec<char>>();
|
||||
let l = b.len() - 1;
|
||||
let mut last_non_zero = b.len() - 1;
|
||||
for i in 0..l {
|
||||
if b[l - i] != '0' {
|
||||
last_non_zero = l - i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if last_non_zero < 2 {
|
||||
return vec![];
|
||||
}
|
||||
b[2..last_non_zero].to_vec()
|
||||
}
|
||||
|
||||
/// Return true if we need to add a separator in position digit_index
|
||||
/// It normally happens if if digit_index -1 is 3, 6, 9,... digit_index ≡ 1 mod 3
|
||||
fn use_group_separator(use_thousands: bool, digit_index: i32, group_sizes: &str) -> bool {
|
||||
if use_thousands {
|
||||
if group_sizes == "#,##0.###" {
|
||||
if digit_index > 1 && (digit_index - 1) % 3 == 0 {
|
||||
return true;
|
||||
}
|
||||
} else if group_sizes == "#,##,##0.###"
|
||||
&& (digit_index == 3 || (digit_index > 3 && digit_index % 2 == 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Formatted {
|
||||
let mut parser = Parser::new(format);
|
||||
parser.parse();
|
||||
let parts = parser.parts;
|
||||
// There are four parts:
|
||||
// 1) Positive numbers
|
||||
// 2) Negative numbers
|
||||
// 3) Zero
|
||||
// 4) Text
|
||||
// If you specify only one section of format code, the code in that section is used for all numbers.
|
||||
// If you specify two sections of format code, the first section of code is used
|
||||
// for positive numbers and zeros, and the second section of code is used for negative numbers.
|
||||
// When you skip code sections in your number format,
|
||||
// you must include a semicolon for each of the missing sections of code.
|
||||
// You can use the ampersand (&) text operator to join, or concatenate, two values.
|
||||
let mut value = value_original;
|
||||
let part;
|
||||
match parts.len() {
|
||||
1 => {
|
||||
part = &parts[0];
|
||||
}
|
||||
2 => {
|
||||
if value >= 0.0 {
|
||||
part = &parts[0]
|
||||
} else {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
if value > 0.0 {
|
||||
part = &parts[0]
|
||||
} else if value < 0.0 {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
} else {
|
||||
value = 0.0;
|
||||
part = &parts[2];
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
if value > 0.0 {
|
||||
part = &parts[0]
|
||||
} else if value < 0.0 {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
} else {
|
||||
value = 0.0;
|
||||
part = &parts[2];
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Too many parts".to_owned()),
|
||||
};
|
||||
}
|
||||
}
|
||||
match part {
|
||||
ParsePart::Error(..) => Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Problem parsing format string".to_owned()),
|
||||
},
|
||||
ParsePart::General(..) => {
|
||||
// FIXME: This is "General formatting"
|
||||
// We should have different codepaths for general formatting and errors
|
||||
let value_abs = value.abs();
|
||||
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
||||
let mut text = format!("{:.9}", value);
|
||||
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
Formatted {
|
||||
text,
|
||||
color: None,
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
if value_abs == 0.0 {
|
||||
return Formatted {
|
||||
text: "0".to_string(),
|
||||
color: None,
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
let exponent = value_abs.log10().floor();
|
||||
value /= 10.0_f64.powf(exponent);
|
||||
let sign = if exponent < 0.0 { '-' } else { '+' };
|
||||
let s = format!("{:.5}", value);
|
||||
Formatted {
|
||||
text: format!(
|
||||
"{}E{}{:02}",
|
||||
s.trim_end_matches('0').trim_end_matches('.'),
|
||||
sign,
|
||||
exponent.abs()
|
||||
),
|
||||
color: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
ParsePart::Date(p) => {
|
||||
let tokens = &p.tokens;
|
||||
let mut text = "".to_string();
|
||||
if !(1.0..=2_958_465.0).contains(&value) {
|
||||
// 2_958_465 is 31 December 9999
|
||||
return Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Date negative or too long".to_owned()),
|
||||
};
|
||||
}
|
||||
let date = from_excel_date(value as i64);
|
||||
for token in tokens {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Digit(_) => {}
|
||||
TextToken::Period => {}
|
||||
TextToken::Day => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{}{}", text, day);
|
||||
}
|
||||
TextToken::DayPadded => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{}{:02}", text, day);
|
||||
}
|
||||
TextToken::DayNameShort => {
|
||||
let mut day = date.weekday().number_from_monday() as usize;
|
||||
if day == 7 {
|
||||
day = 0;
|
||||
}
|
||||
text = format!("{}{}", text, &locale.dates.day_names_short[day]);
|
||||
}
|
||||
TextToken::DayName => {
|
||||
let mut day = date.weekday().number_from_monday() as usize;
|
||||
if day == 7 {
|
||||
day = 0;
|
||||
}
|
||||
text = format!("{}{}", text, &locale.dates.day_names[day]);
|
||||
}
|
||||
TextToken::Month => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, month);
|
||||
}
|
||||
TextToken::MonthPadded => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{:02}", text, month);
|
||||
}
|
||||
TextToken::MonthNameShort => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
|
||||
}
|
||||
TextToken::MonthName => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, &locale.dates.months[month - 1]);
|
||||
}
|
||||
TextToken::MonthLetter => {
|
||||
let month = date.month() as usize;
|
||||
let months_letter = &locale.dates.months_letter[month - 1];
|
||||
text = format!("{}{}", text, months_letter);
|
||||
}
|
||||
TextToken::YearShort => {
|
||||
text = format!("{}{}", text, date.format("%y"));
|
||||
}
|
||||
TextToken::Year => {
|
||||
text = format!("{}{}", text, date.year());
|
||||
}
|
||||
}
|
||||
}
|
||||
Formatted {
|
||||
text,
|
||||
color: p.color,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
ParsePart::Number(p) => {
|
||||
let mut text = "".to_string();
|
||||
let tokens = &p.tokens;
|
||||
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
||||
// p.precision is the number of significant digits _after_ the decimal point
|
||||
value = to_precision(
|
||||
value,
|
||||
(p.precision as usize) + format!("{}", value.abs().floor()).len(),
|
||||
);
|
||||
let mut value_abs = value.abs();
|
||||
let mut exponent_part: Vec<char> = vec![];
|
||||
let mut exponent_is_negative = value_abs < 10.0;
|
||||
if p.is_scientific {
|
||||
if value_abs == 0.0 {
|
||||
exponent_part = vec!['0'];
|
||||
exponent_is_negative = false;
|
||||
} else {
|
||||
// TODO: Implement engineering formatting.
|
||||
let exponent = value_abs.log10().floor();
|
||||
exponent_part = format!("{}", exponent.abs()).chars().collect();
|
||||
value /= 10.0_f64.powf(exponent);
|
||||
value = to_precision(value, 15);
|
||||
value_abs = value.abs();
|
||||
}
|
||||
}
|
||||
let l_exp = exponent_part.len() as i32;
|
||||
let mut int_part: Vec<char> = format!("{}", value_abs.floor()).chars().collect();
|
||||
if value_abs as i64 == 0 {
|
||||
int_part = vec![];
|
||||
}
|
||||
let fract_part = get_fract_part(value_abs, p.precision);
|
||||
// ln is the number of digits of the integer part of the value
|
||||
let ln = int_part.len() as i32;
|
||||
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
|
||||
let digit_count = p.digit_count;
|
||||
// digit_index points to the digit index in value that we have already formatted
|
||||
let mut digit_index = 0;
|
||||
|
||||
let symbols = &locale.numbers.symbols;
|
||||
let group_sizes = locale.numbers.decimal_formats.standard.to_owned();
|
||||
let group_separator = symbols.group.to_owned();
|
||||
let decimal_separator = symbols.decimal.to_owned();
|
||||
// There probably are better ways to check if a number at a given precision is negative :/
|
||||
let is_negative = value < -(10.0_f64.powf(-(p.precision as f64)));
|
||||
|
||||
for token in tokens {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Period => {
|
||||
text = format!("{}{}", text, decimal_separator);
|
||||
}
|
||||
TextToken::Digit(digit) => {
|
||||
if digit.number == 'i' {
|
||||
// 1. Integer part
|
||||
let index = digit.index;
|
||||
let number_index = ln - digit_count + index;
|
||||
if index == 0 && is_negative {
|
||||
text = format!("-{}", text);
|
||||
}
|
||||
if ln <= digit_count {
|
||||
// The number of digits is less or equal than the number of digit tokens
|
||||
// i.e. the value is 123 and the format_code is ##### (ln = 3 and digit_count = 5)
|
||||
if !(number_index < 0 && digit.kind == '#') {
|
||||
let c = if number_index < 0 {
|
||||
if digit.kind == '0' {
|
||||
'0'
|
||||
} else {
|
||||
// digit.kind = '?'
|
||||
' '
|
||||
}
|
||||
} else {
|
||||
int_part[number_index as usize]
|
||||
};
|
||||
let sep = if use_group_separator(
|
||||
p.use_thousands,
|
||||
ln - digit_index,
|
||||
&group_sizes,
|
||||
) {
|
||||
&group_separator
|
||||
} else {
|
||||
""
|
||||
};
|
||||
text = format!("{}{}{}", text, c, sep);
|
||||
}
|
||||
digit_index += 1;
|
||||
} else {
|
||||
// The number is larger than the formatting code 12345 and 0##
|
||||
// We just hit the first formatting digit (0 in the example above) so we write as many digits as we can (123 in the example)
|
||||
for i in digit_index..number_index + 1 {
|
||||
let sep = if use_group_separator(
|
||||
p.use_thousands,
|
||||
ln - i,
|
||||
&group_sizes,
|
||||
) {
|
||||
&group_separator
|
||||
} else {
|
||||
""
|
||||
};
|
||||
text = format!("{}{}{}", text, int_part[i as usize], sep);
|
||||
}
|
||||
digit_index = number_index + 1;
|
||||
}
|
||||
} else if digit.number == 'd' {
|
||||
// 2. After the decimal point
|
||||
let index = digit.index as usize;
|
||||
if index < fract_part.len() {
|
||||
text = format!("{}{}", text, fract_part[index]);
|
||||
} else if digit.kind == '0' {
|
||||
text = format!("{}0", text);
|
||||
} else if digit.kind == '?' {
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
} else if digit.number == 'e' {
|
||||
// 3. Exponent part
|
||||
let index = digit.index;
|
||||
if index == 0 {
|
||||
if exponent_is_negative {
|
||||
text = format!("{}E-", text);
|
||||
} else {
|
||||
text = format!("{}E+", text);
|
||||
}
|
||||
}
|
||||
let number_index = l_exp - (p.exponent_digit_count - index);
|
||||
if l_exp <= p.exponent_digit_count {
|
||||
if !(number_index < 0 && digit.kind == '#') {
|
||||
let c = if number_index < 0 {
|
||||
if digit.kind == '?' {
|
||||
' '
|
||||
} else {
|
||||
'0'
|
||||
}
|
||||
} else {
|
||||
exponent_part[number_index as usize]
|
||||
};
|
||||
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
} else {
|
||||
for i in 0..number_index + 1 {
|
||||
text = format!("{}{}", text, exponent_part[i as usize]);
|
||||
}
|
||||
digit_index += number_index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Date tokens should not be present
|
||||
TextToken::Day => {}
|
||||
TextToken::DayPadded => {}
|
||||
TextToken::DayNameShort => {}
|
||||
TextToken::DayName => {}
|
||||
TextToken::Month => {}
|
||||
TextToken::MonthPadded => {}
|
||||
TextToken::MonthNameShort => {}
|
||||
TextToken::MonthName => {}
|
||||
TextToken::MonthLetter => {}
|
||||
TextToken::YearShort => {}
|
||||
TextToken::Year => {}
|
||||
}
|
||||
}
|
||||
Formatted {
|
||||
text,
|
||||
color: p.color,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_day(day_str: &str) -> Result<(u32, String), String> {
|
||||
let bytes = day_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len <= 2 {
|
||||
match day_str.parse::<u32>() {
|
||||
Ok(y) => {
|
||||
if bytes_len == 2 {
|
||||
return Ok((y, "dd".to_string()));
|
||||
} else {
|
||||
return Ok((y, "d".to_string()));
|
||||
}
|
||||
}
|
||||
Err(_) => return Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
Err("Not a valid day".to_string())
|
||||
}
|
||||
|
||||
fn parse_month(month_str: &str) -> Result<(u32, String), String> {
|
||||
let bytes = month_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len <= 2 {
|
||||
match month_str.parse::<u32>() {
|
||||
Ok(y) => {
|
||||
if bytes_len == 2 {
|
||||
return Ok((y, "mm".to_string()));
|
||||
} else {
|
||||
return Ok((y, "m".to_string()));
|
||||
}
|
||||
}
|
||||
Err(_) => return Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
let month_names_short = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec",
|
||||
];
|
||||
let month_names_long = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
if let Some(m) = month_names_short.iter().position(|&r| r == month_str) {
|
||||
return Ok((m as u32 + 1, "mmm".to_string()));
|
||||
}
|
||||
if let Some(m) = month_names_long.iter().position(|&r| r == month_str) {
|
||||
return Ok((m as u32 + 1, "mmmm".to_string()));
|
||||
}
|
||||
Err("Not a valid day".to_string())
|
||||
}
|
||||
|
||||
fn parse_year(year_str: &str) -> Result<(i32, String), String> {
|
||||
// year is either 2 digits or 4 digits
|
||||
// 23 -> 2023
|
||||
// 75 -> 1975
|
||||
// 30 is the split number (yeah, that's not going to be a problem any time soon)
|
||||
// 30 => 1930
|
||||
// 29 => 2029
|
||||
let bytes = year_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len != 2 && bytes_len != 4 {
|
||||
return Err("Not a valid year".to_string());
|
||||
}
|
||||
match year_str.parse::<i32>() {
|
||||
Ok(y) => {
|
||||
if y < 30 {
|
||||
Ok((2000 + y, "yy".to_string()))
|
||||
} else if y < 100 {
|
||||
Ok((1900 + y, "yy".to_string()))
|
||||
} else {
|
||||
Ok((y, "yyyy".to_string()))
|
||||
}
|
||||
}
|
||||
Err(_) => Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is a date. Other spreadsheet engines support a wide variety of dates formats
|
||||
// Here we support a small subset of them.
|
||||
//
|
||||
// The grammar is:
|
||||
//
|
||||
// date -> long_date | short_date | iso-date
|
||||
// short_date -> month separator year
|
||||
// long_date -> day separator month separator year
|
||||
// iso_date -> long_year separator number_month separator number_day
|
||||
// separator -> "/" | "-"
|
||||
// day -> number | padded number
|
||||
// month -> number_month | name_month
|
||||
// number_month -> number | padded number |
|
||||
// name_month -> short name | full name
|
||||
// year -> short_year | long year
|
||||
//
|
||||
// NOTE 1: The separator has to be the same
|
||||
// NOTE 2: In some engines "2/3" is implemented ad "2/March of the present year"
|
||||
// NOTE 3: I did not implement the "short date"
|
||||
fn parse_date(value: &str) -> Result<(i32, String), String> {
|
||||
let separator = if value.contains('/') {
|
||||
'/'
|
||||
} else if value.contains('-') {
|
||||
'-'
|
||||
} else {
|
||||
return Err("Not a valid date".to_string());
|
||||
};
|
||||
|
||||
let parts: Vec<&str> = value.split(separator).collect();
|
||||
let mut is_iso_date = false;
|
||||
let (day_str, month_str, year_str) = if parts.len() == 3 {
|
||||
if parts[0].len() == 4 {
|
||||
// ISO date yyyy-mm-dd
|
||||
if !parts[1].chars().all(char::is_numeric) {
|
||||
return Err("Not a valid date".to_string());
|
||||
}
|
||||
if !parts[2].chars().all(char::is_numeric) {
|
||||
return Err("Not a valid date".to_string());
|
||||
}
|
||||
is_iso_date = true;
|
||||
(parts[2], parts[1], parts[0])
|
||||
} else {
|
||||
(parts[0], parts[1], parts[2])
|
||||
}
|
||||
} else {
|
||||
return Err("Not a valid date".to_string());
|
||||
};
|
||||
let (day, day_format) = parse_day(day_str)?;
|
||||
let (month, month_format) = parse_month(month_str)?;
|
||||
let (year, year_format) = parse_year(year_str)?;
|
||||
let serial_number = match date_to_serial_number(day, month, year) {
|
||||
Ok(n) => n,
|
||||
Err(_) => return Err("Not a valid date".to_string()),
|
||||
};
|
||||
if is_iso_date {
|
||||
Ok((
|
||||
serial_number,
|
||||
format!("yyyy{separator}{month_format}{separator}{day_format}"),
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
serial_number,
|
||||
format!("{day_format}{separator}{month_format}{separator}{year_format}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a formatted number, returning the numeric value together with the format
|
||||
/// Uses heuristics to guess the format string
|
||||
/// "$ 123,345.678" => (123345.678, "$#,##0.00")
|
||||
/// "30.34%" => (0.3034, "0.00%")
|
||||
/// 100€ => (100, "100€")
|
||||
pub(crate) fn parse_formatted_number(
|
||||
value: &str,
|
||||
currencies: &[&str],
|
||||
) -> Result<(f64, Option<String>), String> {
|
||||
let value = value.trim();
|
||||
let scientific_format = "0.00E+00";
|
||||
|
||||
// Check if it is a percentage
|
||||
if let Some(p) = value.strip_suffix('%') {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f / 100.0, Some(scientific_format.to_string())));
|
||||
}
|
||||
// We ignore the separator
|
||||
if options.decimal_digits > 0 {
|
||||
// Percentage format with decimals
|
||||
return Ok((f / 100.0, Some("#,##0.00%".to_string())));
|
||||
}
|
||||
// Percentage format standard
|
||||
return Ok((f / 100.0, Some("#,##0%".to_string())));
|
||||
}
|
||||
|
||||
// check if it is a currency in currencies
|
||||
for currency in currencies {
|
||||
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
return Ok((-f, Some(format!("{currency}#,##0.00"))));
|
||||
}
|
||||
return Ok((-f, Some(format!("{currency}#,##0"))));
|
||||
} else if let Some(p) = value.strip_prefix(currency) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
return Ok((f, Some(format!("{currency}#,##0.00"))));
|
||||
}
|
||||
return Ok((f, Some(format!("{currency}#,##0"))));
|
||||
} else if let Some(p) = value.strip_suffix(currency) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
let currency_format = &format!("#,##0.00{currency}");
|
||||
return Ok((f, Some(currency_format.to_string())));
|
||||
}
|
||||
let currency_format = &format!("#,##0{currency}");
|
||||
return Ok((f, Some(currency_format.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok((serial_number, format)) = parse_date(value) {
|
||||
return Ok((serial_number as f64, Some(format)));
|
||||
}
|
||||
|
||||
// Lastly we check if it is a number
|
||||
let (f, options) = parse_number(value)?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.has_commas {
|
||||
if options.decimal_digits > 0 {
|
||||
// group separator and two decimal points
|
||||
return Ok((f, Some("#,##0.00".to_string())));
|
||||
}
|
||||
// Group separator and no decimal points
|
||||
return Ok((f, Some("#,##0".to_string())));
|
||||
}
|
||||
Ok((f, None))
|
||||
}
|
||||
|
||||
struct NumberOptions {
|
||||
has_commas: bool,
|
||||
is_scientific: bool,
|
||||
decimal_digits: usize,
|
||||
}
|
||||
|
||||
// tries to parse 'value' as a number.
|
||||
// If it is a number it either uses commas as thousands separator or it does not
|
||||
fn parse_number(value: &str) -> Result<(f64, NumberOptions), String> {
|
||||
let mut position = 0;
|
||||
let bytes = value.as_bytes();
|
||||
let len = bytes.len();
|
||||
if len == 0 {
|
||||
return Err("Cannot parse number".to_string());
|
||||
}
|
||||
let mut chars = String::from("");
|
||||
let decimal_separator = b'.';
|
||||
let group_separator = b',';
|
||||
let mut group_separator_index = Vec::new();
|
||||
// get the sign
|
||||
let sign = if bytes[0] == b'-' {
|
||||
position += 1;
|
||||
-1.0
|
||||
} else if bytes[0] == b'+' {
|
||||
position += 1;
|
||||
1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// numbers before the decimal point
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else if x == group_separator {
|
||||
group_separator_index.push(chars.len());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
// Check the group separator is in multiples of three
|
||||
for index in &group_separator_index {
|
||||
if (chars.len() - index) % 3 != 0 {
|
||||
return Err("Cannot parse number".to_string());
|
||||
}
|
||||
}
|
||||
let mut decimal_digits = 0;
|
||||
if position < len && bytes[position] == decimal_separator {
|
||||
// numbers after the decimal point
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
let start_position = 0;
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
decimal_digits = position - start_position;
|
||||
}
|
||||
let mut is_scientific = false;
|
||||
if position + 1 < len && (bytes[position] == b'e' || bytes[position] == b'E') {
|
||||
// exponential side
|
||||
is_scientific = true;
|
||||
let x = bytes[position + 1];
|
||||
if x == b'-' || x == b'+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x as char);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if position != len {
|
||||
return Err("Could not parse number".to_string());
|
||||
};
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => Err("Failed to parse to double".to_string()),
|
||||
Ok(v) => Ok((
|
||||
sign * v,
|
||||
NumberOptions {
|
||||
has_commas: !group_separator_index.is_empty(),
|
||||
is_scientific,
|
||||
decimal_digits,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
408
base/src/formatter/lexer.rs
Normal file
408
base/src/formatter/lexer.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
pub struct Lexer {
|
||||
position: usize,
|
||||
len: usize,
|
||||
chars: Vec<char>,
|
||||
error_message: String,
|
||||
error_position: usize,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Token {
|
||||
Color(i32), // [Red] or [Color 23]
|
||||
Condition(Compare, f64), // [<=100] (Comparator, number)
|
||||
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
|
||||
Spacer(char), // *X
|
||||
Ghost(char), // _X
|
||||
Text(String), // "Text"
|
||||
Separator, // ;
|
||||
Raw, // @
|
||||
Percent, // %
|
||||
Comma, // ,
|
||||
Period, // .
|
||||
Sharp, // #
|
||||
Zero, // 0
|
||||
QuestionMark, // ?
|
||||
Scientific, // E+
|
||||
ScientificMinus, // E-
|
||||
General, // General
|
||||
// Dates
|
||||
Day, // d
|
||||
DayPadded, // dd
|
||||
DayNameShort, // ddd
|
||||
DayName, // dddd+
|
||||
Month, // m
|
||||
MonthPadded, // mm
|
||||
MonthNameShort, // mmm
|
||||
MonthName, // mmmm or mmmmmm+
|
||||
MonthLetter, // mmmmm
|
||||
YearShort, // y or yy
|
||||
Year, // yyy+
|
||||
// TODO: Hours Minutes and Seconds
|
||||
ILLEGAL,
|
||||
EOF,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum Compare {
|
||||
Equal,
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
LessOrEqualThan,
|
||||
GreaterOrEqualThan,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn is_digit(&self) -> bool {
|
||||
(self == &Token::Zero) || (self == &Token::Sharp) || (self == &Token::QuestionMark)
|
||||
}
|
||||
|
||||
pub fn is_date(&self) -> bool {
|
||||
self == &Token::Day
|
||||
|| self == &Token::DayPadded
|
||||
|| self == &Token::DayNameShort
|
||||
|| self == &Token::DayName
|
||||
|| self == &Token::MonthName
|
||||
|| self == &Token::MonthNameShort
|
||||
|| self == &Token::Month
|
||||
|| self == &Token::MonthPadded
|
||||
|| self == &Token::MonthLetter
|
||||
|| self == &Token::YearShort
|
||||
|| self == &Token::Year
|
||||
}
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
pub fn new(format: &str) -> Lexer {
|
||||
let chars: Vec<char> = format.chars().collect();
|
||||
let len = chars.len();
|
||||
Lexer {
|
||||
chars,
|
||||
position: 0,
|
||||
len,
|
||||
error_message: "".to_string(),
|
||||
error_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn read_next_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
self.position = position + 1;
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: &str) {
|
||||
self.error_message = error.to_string();
|
||||
self.error_position = self.position;
|
||||
self.position = self.len;
|
||||
}
|
||||
|
||||
fn consume_string(&mut self) -> Option<String> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
position += 1;
|
||||
if x != '"' {
|
||||
chars.push(x);
|
||||
} else if position < len && self.chars[position] == '"' {
|
||||
chars.push(x);
|
||||
chars.push(self.chars[position]);
|
||||
position += 1;
|
||||
} else {
|
||||
self.position = position;
|
||||
return Some(chars);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn consume_number(&mut self) -> Option<f64> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
// numbers before the '.'
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position < len && self.chars[position] == '.' {
|
||||
// numbers after the'.'
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
if position + 1 < len && self.chars[position] == 'e' {
|
||||
// exponential side
|
||||
let x = self.chars[position + 1];
|
||||
if x == '-' || x == '+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => None,
|
||||
Ok(v) => Some(v),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_condition(&mut self) -> Option<(Compare, f64)> {
|
||||
let cmp;
|
||||
match self.read_next_char() {
|
||||
Some('<') => {
|
||||
if let Some('=') = self.peek_char() {
|
||||
self.read_next_char();
|
||||
cmp = Compare::LessOrEqualThan;
|
||||
} else {
|
||||
cmp = Compare::LessThan;
|
||||
}
|
||||
}
|
||||
Some('>') => {
|
||||
if let Some('=') = self.peek_char() {
|
||||
self.read_next_char();
|
||||
cmp = Compare::GreaterOrEqualThan;
|
||||
} else {
|
||||
cmp = Compare::GreaterThan;
|
||||
}
|
||||
}
|
||||
Some('=') => {
|
||||
cmp = Compare::Equal;
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if let Some(v) = self.consume_number() {
|
||||
return Some((cmp, v));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn consume_color(&mut self) -> Option<i32> {
|
||||
let colors = [
|
||||
"black", "white", "red", "green", "blue", "yellow", "magenta",
|
||||
];
|
||||
let mut chars = "".to_string();
|
||||
while let Some(ch) = self.read_next_char() {
|
||||
if ch == ']' {
|
||||
if let Some(index) = colors.iter().position(|&x| x == chars.to_lowercase()) {
|
||||
return Some(index as i32);
|
||||
}
|
||||
if !chars.starts_with("Color") {
|
||||
return None;
|
||||
}
|
||||
if let Ok(index) = chars[5..].trim().parse::<i32>() {
|
||||
if index < 57 && index > 0 {
|
||||
return Some(index);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
return None;
|
||||
} else {
|
||||
chars.push(ch);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn peek_token(&mut self) -> Token {
|
||||
let position = self.position;
|
||||
let token = self.next_token();
|
||||
self.position = position;
|
||||
token
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Token {
|
||||
let ch = self.read_next_char();
|
||||
match ch {
|
||||
Some(x) => match x {
|
||||
'$' | '€' | '(' | ')' | '/' | ':' | '+' | '-' | '^' | '\'' | '{' | '}' | '<'
|
||||
| '=' | '!' | '~' | '>' | ' ' => Token::Literal(x),
|
||||
'?' => Token::QuestionMark,
|
||||
';' => Token::Separator,
|
||||
'#' => Token::Sharp,
|
||||
',' => Token::Comma,
|
||||
'.' => Token::Period,
|
||||
'0' => Token::Zero,
|
||||
'@' => Token::Raw,
|
||||
'%' => Token::Percent,
|
||||
'[' => {
|
||||
if let Some(c) = self.peek_char() {
|
||||
if c == '<' || c == '>' || c == '=' {
|
||||
// Condition
|
||||
if let Some((cmp, value)) = self.consume_condition() {
|
||||
Token::Condition(cmp, value)
|
||||
} else {
|
||||
self.set_error("Failed to parse condition");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
// Color
|
||||
if let Some(index) = self.consume_color() {
|
||||
return Token::Color(index);
|
||||
}
|
||||
self.set_error("Failed to parse color");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'_' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Ghost(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'*' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Spacer(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'\\' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Literal(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
if let Some(s) = self.consume_string() {
|
||||
Token::Text(s)
|
||||
} else {
|
||||
self.set_error("Did not find end of text string");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'E' => {
|
||||
if let Some(s) = self.read_next_char() {
|
||||
if s == '+' {
|
||||
Token::Scientific
|
||||
} else if s == '-' {
|
||||
Token::ScientificMinus
|
||||
} else {
|
||||
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'd' => {
|
||||
let mut d = 1;
|
||||
while let Some('d') = self.peek_char() {
|
||||
d += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
match d {
|
||||
1 => Token::Day,
|
||||
2 => Token::DayPadded,
|
||||
3 => Token::DayNameShort,
|
||||
_ => Token::DayName,
|
||||
}
|
||||
}
|
||||
'm' => {
|
||||
let mut m = 1;
|
||||
while let Some('m') = self.peek_char() {
|
||||
m += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
match m {
|
||||
1 => Token::Month,
|
||||
2 => Token::MonthPadded,
|
||||
3 => Token::MonthNameShort,
|
||||
4 => Token::MonthName,
|
||||
5 => Token::MonthLetter,
|
||||
_ => Token::MonthName,
|
||||
}
|
||||
}
|
||||
'y' => {
|
||||
let mut y = 1;
|
||||
while let Some('y') = self.peek_char() {
|
||||
y += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
if y == 1 || y == 2 {
|
||||
Token::YearShort
|
||||
} else {
|
||||
Token::Year
|
||||
}
|
||||
}
|
||||
'g' | 'G' => {
|
||||
for c in "eneral".chars() {
|
||||
let cc = self.read_next_char();
|
||||
if Some(c) != cc {
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
return Token::ILLEGAL;
|
||||
}
|
||||
}
|
||||
Token::General
|
||||
}
|
||||
_ => {
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
},
|
||||
None => Token::EOF,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_likely_date_number_format(format: &str) -> bool {
|
||||
let mut lexer = Lexer::new(format);
|
||||
loop {
|
||||
let token = lexer.next_token();
|
||||
if token == Token::EOF {
|
||||
return false;
|
||||
}
|
||||
if token.is_date() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
base/src/formatter/mod.rs
Normal file
105
base/src/formatter/mod.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
pub mod dates;
|
||||
pub mod format;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
// Excel formatting is extremely tricky and I think implementing all it's rules might be borderline impossible.
|
||||
// But the essentials are easy to understand.
|
||||
//
|
||||
// A general Excel formatting string is divided iun four parts:
|
||||
//
|
||||
// <POSITIVE>;<NEGATIVE>;<ZERO>;<TEXT>
|
||||
//
|
||||
// * How many decimal digits do you need?
|
||||
//
|
||||
// 0.000 for exactly three
|
||||
// 0.00??? for at least two and up to five
|
||||
//
|
||||
// * Do you need a thousands separator?
|
||||
//
|
||||
// #,##
|
||||
// # will just write the number
|
||||
// #, will write the number up to the thousand separator (if there is nothing else)
|
||||
//
|
||||
// But #,# and any number of '#' to the right will work just as good. So the following all produce the same results:
|
||||
// #,##0.00 #,######0.00 #,0.00
|
||||
//
|
||||
// For us in IronCalc the most general format string for a number (non-scientific notation) will be:
|
||||
//
|
||||
// 1. Will have #,## at the beginning if we use the thousand separator
|
||||
// 2. Then 0.0* with as many 0 as mandatory decimal places
|
||||
// 3. Then ?* with as many question marks as possible decimal places
|
||||
//
|
||||
// Valid examples:
|
||||
// #,##0.??? Thousand separator, up to three decimal digits
|
||||
// 0.00 No thousand separator. Two mandatory decimal places
|
||||
// 0.0? No thousand separator. One mandatory decimal digit and one extra if present.
|
||||
//
|
||||
// * Do you what the text in color?
|
||||
//
|
||||
// Use [RED] or any color in https://www.excelsupersite.com/what-are-the-56-colorindex-colors-in-excel/
|
||||
|
||||
// Weird things
|
||||
// ============
|
||||
//
|
||||
// ####0.0E+00 of 12345467.890123 (changing the number of '#' produces results I do not understand)
|
||||
// ?www??.????0220000 will format 1234567.890123 to 12345www67.89012223000
|
||||
//
|
||||
// Things we will not implement
|
||||
// ============================
|
||||
//
|
||||
// 1.- The accounting format can leave white spaces of the size of a particular character. For instance:
|
||||
//
|
||||
// #,##0.00_);[Red](#,##0.00)
|
||||
//
|
||||
// Will leave a white space to the right of positive numbers so that they are always aligned with negative numbers
|
||||
//
|
||||
// 2.- Excel can repeat a character as many times as needed to fill the cell:
|
||||
//
|
||||
// _($* #,##0_);_($* (#,##0))
|
||||
//
|
||||
// This will put a '$' sign to the left most (leaving a space the size of '(') and then as many empty spaces as possible
|
||||
// and then the number:
|
||||
// | $ 234 |
|
||||
// | $ 1234 |
|
||||
// We can't do this easily in IronCalc
|
||||
//
|
||||
// 3.- You can use ?/? to format fractions in Excel (this is probably not too hard)
|
||||
|
||||
// TOKENs
|
||||
// ======
|
||||
//
|
||||
// * Color [Red] or [Color 23] or [Color23]
|
||||
// * Conditions [<100]
|
||||
// * Space _X when X is any given char
|
||||
// * A spacer of chars: *X where X is repeated as much as possible
|
||||
// * Literals: $, (, ), :, +, - and space
|
||||
// * Text: "Some Text"
|
||||
// * Escaped char: \X where X is anything
|
||||
// * % appears literal and multiplies number by 100
|
||||
// * , If it's in between digit characters it uses the thousand separator. If it is after the digit characters it multiplies by 1000
|
||||
// * Digit characters: 0, #, ?
|
||||
// * ; Types formatter divider
|
||||
// * @ inserts raw text
|
||||
// * Scientific literals E+, E-, e+, e-
|
||||
// * . period. First one is the decimal point, subsequent are literals.
|
||||
|
||||
// d day of the month
|
||||
// dd day of the month (padded i.e 05)
|
||||
// ddd day of the week abbreviation
|
||||
// dddd+ day of the week
|
||||
// mmm Abbreviation month
|
||||
// mmmm Month name
|
||||
// mmmmm First letter of the month
|
||||
// y or yy 2-digit year
|
||||
// yyy+ 4 digit year
|
||||
|
||||
// References
|
||||
// ==========
|
||||
//
|
||||
// [1] https://support.microsoft.com/en-us/office/number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68?ui=en-us&rs=en-us&ad=us
|
||||
// [2] https://developers.google.com/sheets/api/guides/formats
|
||||
// [3] https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/0e59abdb-7f4e-48fc-9b89-67832fa11789
|
||||
297
base/src/formatter/parser.rs
Normal file
297
base/src/formatter/parser.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use super::lexer::{Compare, Lexer, Token};
|
||||
|
||||
pub struct Digit {
|
||||
pub kind: char, // '#' | '?' | '0'
|
||||
pub index: i32,
|
||||
pub number: char, // 'i' | 'd' | 'e' (integer, decimal or exponent)
|
||||
}
|
||||
|
||||
pub enum TextToken {
|
||||
Literal(char),
|
||||
Text(String),
|
||||
Ghost(char),
|
||||
Spacer(char),
|
||||
// Text
|
||||
Raw,
|
||||
Digit(Digit),
|
||||
Period,
|
||||
// Dates
|
||||
Day,
|
||||
DayPadded,
|
||||
DayNameShort,
|
||||
DayName,
|
||||
Month,
|
||||
MonthPadded,
|
||||
MonthNameShort,
|
||||
MonthName,
|
||||
MonthLetter,
|
||||
YearShort,
|
||||
Year,
|
||||
}
|
||||
pub struct NumberPart {
|
||||
pub color: Option<i32>,
|
||||
pub condition: Option<(Compare, f64)>,
|
||||
pub use_thousands: bool,
|
||||
pub percent: i32, // multiply number by 100^percent
|
||||
pub comma: i32, // divide number by 1000^comma
|
||||
pub tokens: Vec<TextToken>,
|
||||
pub digit_count: i32, // number of digit tokens (#, 0 or ?) to the left of the decimal point
|
||||
pub precision: i32, // number of digits to the right of the decimal point
|
||||
pub is_scientific: bool,
|
||||
pub scientific_minus: bool,
|
||||
pub exponent_digit_count: i32,
|
||||
}
|
||||
|
||||
pub struct DatePart {
|
||||
pub color: Option<i32>,
|
||||
pub tokens: Vec<TextToken>,
|
||||
}
|
||||
|
||||
pub struct ErrorPart {}
|
||||
|
||||
pub struct GeneralPart {}
|
||||
|
||||
pub enum ParsePart {
|
||||
Number(NumberPart),
|
||||
Date(DatePart),
|
||||
Error(ErrorPart),
|
||||
General(GeneralPart),
|
||||
}
|
||||
|
||||
pub struct Parser {
|
||||
pub parts: Vec<ParsePart>,
|
||||
lexer: Lexer,
|
||||
}
|
||||
|
||||
impl ParsePart {
|
||||
pub fn is_error(&self) -> bool {
|
||||
match &self {
|
||||
ParsePart::Date(..) => false,
|
||||
ParsePart::Number(..) => false,
|
||||
ParsePart::Error(..) => true,
|
||||
ParsePart::General(..) => false,
|
||||
}
|
||||
}
|
||||
pub fn is_date(&self) -> bool {
|
||||
match &self {
|
||||
ParsePart::Date(..) => true,
|
||||
ParsePart::Number(..) => false,
|
||||
ParsePart::Error(..) => false,
|
||||
ParsePart::General(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(format: &str) -> Self {
|
||||
let lexer = Lexer::new(format);
|
||||
let parts = vec![];
|
||||
Parser { parts, lexer }
|
||||
}
|
||||
pub fn parse(&mut self) {
|
||||
while self.lexer.peek_token() != Token::EOF {
|
||||
let part = self.parse_part();
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_part(&mut self) -> ParsePart {
|
||||
let mut token = self.lexer.next_token();
|
||||
let mut digit_count = 0;
|
||||
let mut precision = 0;
|
||||
let mut is_date = false;
|
||||
let mut is_number = false;
|
||||
let mut found_decimal_dot = false;
|
||||
let mut use_thousands = false;
|
||||
let mut comma = 0;
|
||||
let mut percent = 0;
|
||||
let mut last_token_is_digit = false;
|
||||
let mut color = None;
|
||||
let mut condition = None;
|
||||
let mut tokens = vec![];
|
||||
let mut is_scientific = false;
|
||||
let mut scientific_minus = false;
|
||||
let mut exponent_digit_count = 0;
|
||||
let mut number = 'i';
|
||||
let mut index = 0;
|
||||
|
||||
while token != Token::EOF && token != Token::Separator {
|
||||
let next_token = self.lexer.next_token();
|
||||
let token_is_digit = token.is_digit();
|
||||
is_number = is_number || token_is_digit;
|
||||
let next_token_is_digit = next_token.is_digit();
|
||||
if token_is_digit {
|
||||
if is_scientific {
|
||||
exponent_digit_count += 1;
|
||||
} else if found_decimal_dot {
|
||||
precision += 1;
|
||||
} else {
|
||||
digit_count += 1;
|
||||
}
|
||||
}
|
||||
match token {
|
||||
Token::General => {
|
||||
if tokens.is_empty() {
|
||||
return ParsePart::General(GeneralPart {});
|
||||
} else {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
}
|
||||
Token::Comma => {
|
||||
// If it is in between digit token then we use the thousand separator
|
||||
if last_token_is_digit && next_token_is_digit {
|
||||
use_thousands = true;
|
||||
} else if digit_count > 0 {
|
||||
comma += 1;
|
||||
} else {
|
||||
// Before the number is just a literal.
|
||||
tokens.push(TextToken::Literal(','));
|
||||
}
|
||||
}
|
||||
Token::Percent => {
|
||||
tokens.push(TextToken::Literal('%'));
|
||||
percent += 1;
|
||||
}
|
||||
Token::Period => {
|
||||
if !found_decimal_dot {
|
||||
tokens.push(TextToken::Period);
|
||||
found_decimal_dot = true;
|
||||
if number == 'i' {
|
||||
number = 'd';
|
||||
index = 0;
|
||||
}
|
||||
} else {
|
||||
tokens.push(TextToken::Literal('.'));
|
||||
}
|
||||
}
|
||||
Token::Color(index) => {
|
||||
color = Some(index);
|
||||
}
|
||||
Token::Condition(cmp, value) => {
|
||||
condition = Some((cmp, value));
|
||||
}
|
||||
Token::QuestionMark => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '?',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Sharp => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '#',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Zero => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '0',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Literal(value) => {
|
||||
tokens.push(TextToken::Literal(value));
|
||||
}
|
||||
Token::Text(value) => {
|
||||
tokens.push(TextToken::Text(value));
|
||||
}
|
||||
Token::Ghost(value) => {
|
||||
tokens.push(TextToken::Ghost(value));
|
||||
}
|
||||
Token::Spacer(value) => {
|
||||
tokens.push(TextToken::Spacer(value));
|
||||
}
|
||||
Token::Day => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Day);
|
||||
}
|
||||
Token::DayPadded => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayPadded);
|
||||
}
|
||||
Token::DayNameShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayNameShort);
|
||||
}
|
||||
Token::DayName => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayName);
|
||||
}
|
||||
Token::MonthNameShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthNameShort);
|
||||
}
|
||||
Token::MonthName => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthName);
|
||||
}
|
||||
Token::Month => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Month);
|
||||
}
|
||||
Token::MonthPadded => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthPadded);
|
||||
}
|
||||
Token::MonthLetter => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthLetter);
|
||||
}
|
||||
Token::YearShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::YearShort);
|
||||
}
|
||||
Token::Year => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Year);
|
||||
}
|
||||
Token::Scientific => {
|
||||
if !is_scientific {
|
||||
index = 0;
|
||||
number = 'e';
|
||||
}
|
||||
is_scientific = true;
|
||||
}
|
||||
Token::ScientificMinus => {
|
||||
is_scientific = true;
|
||||
scientific_minus = true;
|
||||
}
|
||||
Token::Separator => {}
|
||||
Token::Raw => {
|
||||
tokens.push(TextToken::Raw);
|
||||
}
|
||||
Token::ILLEGAL => {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
Token::EOF => {}
|
||||
}
|
||||
last_token_is_digit = token_is_digit;
|
||||
token = next_token;
|
||||
}
|
||||
if is_date {
|
||||
if is_number {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
ParsePart::Date(DatePart { color, tokens })
|
||||
} else {
|
||||
ParsePart::Number(NumberPart {
|
||||
color,
|
||||
condition,
|
||||
use_thousands,
|
||||
percent,
|
||||
comma,
|
||||
tokens,
|
||||
digit_count,
|
||||
precision,
|
||||
is_scientific,
|
||||
scientific_minus,
|
||||
exponent_digit_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
2
base/src/formatter/test/mod.rs
Normal file
2
base/src/formatter/test/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod test_general;
|
||||
mod test_parse_formatted_number;
|
||||
196
base/src/formatter/test/test_general.rs
Normal file
196
base/src/formatter/test/test_general.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
formatter::format::format_number,
|
||||
locale::{get_locale, Locale},
|
||||
};
|
||||
|
||||
fn get_default_locale() -> &'static Locale {
|
||||
get_locale("en").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
let locale = get_default_locale();
|
||||
let bond_james_bond = format_number(7.0, "000", locale);
|
||||
assert_eq!(bond_james_bond.text, "007");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_general() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(7.0, "General", locale).text, "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test_comma() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(1007.0, "000", locale).text, "1007");
|
||||
assert_eq!(format_number(1008.0, "#", locale).text, "1008");
|
||||
assert_eq!(format_number(1009.0, "#,#", locale).text, "1,009");
|
||||
assert_eq!(
|
||||
format_number(12_345_678.0, "#,#", locale).text,
|
||||
"12,345,678"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(12_345_678.0, "0,0", locale).text,
|
||||
"12,345,678"
|
||||
);
|
||||
assert_eq!(format_number(1005.0, "00-00", locale).text, "10-05");
|
||||
assert_eq!(format_number(7.0, "0?0", locale).text, "0 7");
|
||||
assert_eq!(format_number(7.0, "0#0", locale).text, "07");
|
||||
assert_eq!(
|
||||
format_number(1234.0, "000 \"Millions\"", locale).text,
|
||||
"1234 Millions"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(1235.0, "#,000 \"Millions\"", locale).text,
|
||||
"1,235 Millions"
|
||||
);
|
||||
assert_eq!(format_number(1007.0, "0,00", locale).text, "1,007");
|
||||
assert_eq!(
|
||||
format_number(10_000_007.0, "0,00", locale).text,
|
||||
"10,000,007"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_numbers() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(-123.0, "0.0", locale).text, "-123.0");
|
||||
assert_eq!(format_number(-3.0, "000.0", locale).text, "-003.0");
|
||||
assert_eq!(format_number(-0.00001, "000.0", locale).text, "000.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal_part() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "0.00", locale).text, "3.10");
|
||||
assert_eq!(format_number(3.1, "00-.-0?0", locale).text, "03-.-1 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "[blue]0.00", locale).text, "3.10");
|
||||
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parts() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "0.00;(0.00);(-)", locale).text, "3.10");
|
||||
assert_eq!(
|
||||
format_number(-3.1, "0.00;(0.00);(-)", locale).text,
|
||||
"(3.10)"
|
||||
);
|
||||
assert_eq!(format_number(0.0, "0.00;(0.00);(-)", locale).text, "(-)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(0.0, "$#,##0", locale).text, "$0");
|
||||
assert_eq!(format_number(-1.0 / 3.0, "0", locale).text, "0");
|
||||
assert_eq!(format_number(-1.0 / 3.0, "0;(0)", locale).text, "(0)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_currencies() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(-23.0, "$#,##0", locale).text, "-$23");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(0.12, "0.00%", locale).text, "12.00%");
|
||||
assert_eq!(format_number(0.12, "0.00%%", locale).text, "1200.00%%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_correct_rounding() {
|
||||
let locale = get_default_locale();
|
||||
// Formatting does Excel rounding (15 significant digits)
|
||||
assert_eq!(
|
||||
format_number(0.1399999999999999, "0.0%", locale).text,
|
||||
"14.0%"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(-0.1399999999999999, "0.0%", locale).text,
|
||||
"-14.0%"
|
||||
);
|
||||
// Formatting does proper rounding
|
||||
assert_eq!(format_number(0.1399, "0.0%", locale).text, "14.0%");
|
||||
assert_eq!(format_number(-0.1399, "0.0%", locale).text, "-14.0%");
|
||||
assert_eq!(format_number(0.02666, "0.00%", locale).text, "2.67%");
|
||||
assert_eq!(format_number(0.0266, "0.00%", locale).text, "2.66%");
|
||||
assert_eq!(format_number(0.0233, "0.00%", locale).text, "2.33%");
|
||||
assert_eq!(format_number(0.02666, "0%", locale).text, "3%");
|
||||
assert_eq!(format_number(-0.02666, "0.00%", locale).text, "-2.67%");
|
||||
assert_eq!(format_number(-0.02666, "0%", locale).text, "-3%");
|
||||
|
||||
// precision 0
|
||||
assert_eq!(format_number(0.135, "0%", locale).text, "14%");
|
||||
assert_eq!(format_number(0.13499, "0%", locale).text, "13%");
|
||||
assert_eq!(format_number(-0.135, "0%", locale).text, "-14%");
|
||||
assert_eq!(format_number(-0.13499, "0%", locale).text, "-13%");
|
||||
|
||||
// precision 1
|
||||
assert_eq!(format_number(0.1345, "0.0%", locale).text, "13.5%");
|
||||
assert_eq!(format_number(0.1343, "0.0%", locale).text, "13.4%");
|
||||
assert_eq!(format_number(-0.1345, "0.0%", locale).text, "-13.5%");
|
||||
assert_eq!(format_number(-0.134499, "0.0%", locale).text, "-13.4%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(2.5e-14, "0.00E+0", locale).text, "2.50E-14");
|
||||
assert_eq!(format_number(3e-4, "0.00E+00", locale).text, "3.00E-04");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(123.1, "$#,##0", locale).text, "$123");
|
||||
assert_eq!(format_number(123.1, "#,##0 €", locale).text, "123 €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dd/mm/yyyy", locale).text,
|
||||
"29/09/2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dd-mm-yyyy", locale).text,
|
||||
"29-09-2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41304.0, "dd-mm-yyyy", locale).text,
|
||||
"30-01-2013"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(42657.0, "dd-mm-yyyy", locale).text,
|
||||
"14-10-2016"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dddd-mmmm-yyyy", locale).text,
|
||||
"Saturday-September-2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmm-yy", locale).text,
|
||||
"Sat-Sep-12"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmmmm-yy", locale).text,
|
||||
"Sat-S-12"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmmmmmm-yy", locale).text,
|
||||
"Sat-September-12"
|
||||
);
|
||||
}
|
||||
206
base/src/formatter/test/test_parse_formatted_number.rs
Normal file
206
base/src/formatter/test/test_parse_formatted_number.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::formatter::format::parse_formatted_number as parse;
|
||||
|
||||
const PARSE_ERROR_MSG: &str = "Could not parse number";
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
// whole numbers
|
||||
assert_eq!(parse("400", &["$"]), Ok((400.0, None)));
|
||||
|
||||
// decimal numbers
|
||||
assert_eq!(parse("4.456", &["$"]), Ok((4.456, None)));
|
||||
|
||||
// scientific notation
|
||||
assert_eq!(
|
||||
parse("23e-12", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.123456789e-11", &["$"]),
|
||||
Ok((2.123456789e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("4.5E-9", &["$"]),
|
||||
Ok((4.5e-9, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("23e+2", &["$"]),
|
||||
Ok((2300.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("4.5E9", &["$"]),
|
||||
Ok((4.5e9, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// negative numbers
|
||||
assert_eq!(parse("-400", &["$"]), Ok((-400.0, None)));
|
||||
assert_eq!(parse("-4.456", &["$"]), Ok((-4.456, None)));
|
||||
assert_eq!(
|
||||
parse("-23e-12", &["$"]),
|
||||
Ok((-2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// trims space
|
||||
assert_eq!(parse(" 400 ", &["$"]), Ok((400.0, None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage() {
|
||||
// whole numbers
|
||||
assert_eq!(parse("400%", &["$"]), Ok((4.0, Some("#,##0%".to_string()))));
|
||||
// decimal numbers
|
||||
assert_eq!(
|
||||
parse("4.456$", &["$"]),
|
||||
Ok((4.456, Some("#,##0.00$".to_string())))
|
||||
);
|
||||
// Percentage in scientific notation will not be formatted as percentage
|
||||
assert_eq!(
|
||||
parse("23e-12%", &["$"]),
|
||||
Ok((23e-12 / 100.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.3E4%", &["$"]),
|
||||
Ok((230.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency() {
|
||||
// whole numbers
|
||||
assert_eq!(
|
||||
parse("400$", &["$"]),
|
||||
Ok((400.0, Some("#,##0$".to_string())))
|
||||
);
|
||||
// decimal numbers
|
||||
assert_eq!(
|
||||
parse("4.456$", &["$"]),
|
||||
Ok((4.456, Some("#,##0.00$".to_string())))
|
||||
);
|
||||
// Currencies in scientific notation will not be formatted as currencies
|
||||
assert_eq!(
|
||||
parse("23e-12$", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.3e-12$", &["$"]),
|
||||
Ok((2.3e-12, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("€23e-12", &["€"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// switch side of currencies
|
||||
assert_eq!(
|
||||
parse("$400", &["$"]),
|
||||
Ok((400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$4.456", &["$"]),
|
||||
Ok((4.456, Some("$#,##0.00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$23e-12", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$2.3e-12", &["$"]),
|
||||
Ok((2.3e-12, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("23e-12€", &["€"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_currencies() {
|
||||
assert_eq!(
|
||||
parse("-400$", &["$"]),
|
||||
Ok((-400.0, Some("#,##0$".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("-$400", &["$"]),
|
||||
Ok((-400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$-400", &["$"]),
|
||||
Ok((-400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
// Strings are not numbers
|
||||
assert_eq!(parse("One", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
// Not partial parsing
|
||||
assert_eq!(parse("23 Hello", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
assert_eq!(parse("Hello 23", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
assert_eq!(parse("2 3", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
// No space between
|
||||
assert_eq!(parse("- 23", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_wrong_currency() {
|
||||
assert_eq!(parse("123€", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_dates() {
|
||||
assert_eq!(
|
||||
parse("02/03/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/3/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/m/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/Mar/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mmm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/March/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mmmm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2/3/24", &["$"]),
|
||||
Ok((45353.0, Some("d/m/yy".to_string())))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse("10-02-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-2-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-m-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-Feb-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mmm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-February-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mmmm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-2-75", &["$"]),
|
||||
Ok((27435.0, Some("dd-m-yy".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso_dates() {
|
||||
assert_eq!(
|
||||
parse("2024/03/02", &["$"]),
|
||||
Ok((45353.0, Some("yyyy/mm/dd".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2024/March/02", &["$"]),
|
||||
Err(PARSE_ERROR_MSG.to_string())
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user