UPDATE: Dump of initial files

This commit is contained in:
Nicolás Hatcher
2023-11-18 21:26:18 +01:00
commit c5b8efd83d
279 changed files with 42654 additions and 0 deletions

View 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()),
}
}

View 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
View 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
View 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

View 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,
})
}
}
}

View File

@@ -0,0 +1,2 @@
mod test_general;
mod test_parse_formatted_number;

View 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"
);
}

View 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())
);
}