1use std::fmt;
48use std::sync::Arc;
49
50use crate::calc::Value;
51
52pub const MOST_CHARS: usize = 255;
54
55pub const MOST_DECIMALS: usize = 30;
57
58const BUILTINS: [(&str, u32); 8] = [
63 ("0", 1),
64 ("0.00", 2),
65 ("#,##0", 3),
66 ("#,##0.00", 4),
67 ("0%", 9),
68 ("0.00%", 10),
69 ("#,##0 ;(#,##0)", 37),
70 ("#,##0.00;(#,##0.00)", 39),
71];
72
73#[derive(Clone)]
76pub struct Code(Arc<Parsed>);
77
78struct Parsed {
79 text: String,
80 sections: Vec<Section>,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct Unsupported {
87 pub what: String,
88 pub fix: String,
89}
90
91impl Unsupported {
92 fn new(what: impl Into<String>, fix: impl Into<String>) -> Unsupported {
93 Unsupported {
94 what: what.into(),
95 fix: fix.into(),
96 }
97 }
98}
99
100#[derive(Clone, Debug, PartialEq)]
102enum Part {
103 Digit(bool),
106 Point,
107 Percent,
108 Literal(String),
109 At,
111}
112
113#[derive(Clone, Debug, Default)]
115struct Section {
116 parts: Vec<Part>,
117 placeholders: bool,
119 integer: usize,
121 min_integer: usize,
124 decimals: Vec<bool>,
126 grouping: bool,
128 shift: i32,
131}
132
133#[derive(Clone, Debug, PartialEq)]
135enum Token {
136 Digit(bool),
137 Point,
138 Comma,
139 Percent,
140 Literal(String),
141 At,
142}
143
144impl Code {
145 pub fn parse(text: &str) -> Result<Code, Unsupported> {
147 if text.is_empty() {
148 return Err(Unsupported::new(
149 "no code at all",
150 "write general for Excel's General format",
151 ));
152 }
153 if text.eq_ignore_ascii_case("general") {
154 return Err(Unsupported::new(
155 "Excel's General format as a code",
156 "write it general, a symbol, unquoted",
157 ));
158 }
159 if text == "@" {
160 return Err(Unsupported::new(
161 "Excel's Text format as a code",
162 "write it text, a symbol, unquoted",
163 ));
164 }
165 if text.chars().count() > MOST_CHARS {
166 return Err(Unsupported::new(
167 format!("more than {MOST_CHARS} characters"),
168 format!("shorten it to Excel's {MOST_CHARS}"),
169 ));
170 }
171 let raw = tokens(text)?;
172 if raw.len() > 4 {
173 return Err(Unsupported::new(
174 "a fifth section",
175 "keep four, a positive number's, a negative's, zero's and a text's",
176 ));
177 }
178 let mut sections = Vec::with_capacity(raw.len());
179 for (i, toks) in raw.into_iter().enumerate() {
180 sections.push(if i == 3 {
181 text_section(toks)?
182 } else {
183 number_section(toks)?
184 });
185 }
186 Ok(Code(Arc::new(Parsed {
187 text: text.to_string(),
188 sections,
189 })))
190 }
191
192 pub fn as_str(&self) -> &str {
194 &self.0.text
195 }
196
197 pub fn builtin(&self) -> Option<u32> {
200 BUILTINS
201 .iter()
202 .find(|(code, _)| *code == self.0.text)
203 .map(|&(_, id)| id)
204 }
205
206 pub fn text(&self, value: &Value) -> Option<String> {
212 match value {
213 Value::Number(n) => self.number(*n),
214 Value::Text(t) => self.0.sections.get(3).map(|s| s.text_of(t)),
215 _ => None,
216 }
217 }
218
219 fn number(&self, x: f64) -> Option<String> {
220 if !x.is_finite() {
221 return None;
222 }
223 let sections = &self.0.sections;
224 let numeric = sections.len().min(3);
225 let (section, minus) = if x == 0.0 {
226 (§ions[if numeric == 3 { 2 } else { 0 }], false)
228 } else if x < 0.0 {
229 if numeric >= 2 {
230 (§ions[1], false)
231 } else {
232 (§ions[0], true)
233 }
234 } else {
235 (§ions[0], false)
236 };
237 let mut out = String::new();
238 let zero = section.draw(x.abs(), &mut out);
239 if minus && !zero {
243 out.insert(0, '-');
244 }
245 Some(out)
246 }
247}
248
249impl PartialEq for Code {
250 fn eq(&self, other: &Code) -> bool {
251 self.0.text == other.0.text
252 }
253}
254
255impl Eq for Code {}
256
257impl fmt::Debug for Code {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.debug_tuple("Code").field(&self.0.text).finish()
260 }
261}
262
263fn tokens(text: &str) -> Result<Vec<Vec<Token>>, Unsupported> {
266 let chars: Vec<char> = text.chars().collect();
267 let mut sections: Vec<Vec<Token>> = vec![Vec::new()];
268 let mut i = 0;
269 while i < chars.len() {
270 let c = chars[i];
271 let toks = sections.last_mut().expect("one section at least");
272 match c {
273 ';' => sections.push(Vec::new()),
274 '"' => {
275 let start = i + 1;
276 let Some(len) = chars[start..].iter().position(|&x| x == '"') else {
277 return Err(Unsupported::new("a quote left open", "close it"));
278 };
279 let quoted: String = chars[start..start + len].iter().collect();
280 if quoted.chars().any(is_control) {
281 return Err(control());
282 }
283 toks.push(Token::Literal(quoted));
284 i = start + len;
285 }
286 '\\' => {
287 let Some(&next) = chars.get(i + 1) else {
288 return Err(Unsupported::new(
289 "a backslash with nothing after it",
290 "drop it, or write after it the character it draws",
291 ));
292 };
293 if is_control(next) {
294 return Err(control());
295 }
296 toks.push(Token::Literal(next.to_string()));
297 i += 1;
298 }
299 '0' => toks.push(Token::Digit(true)),
300 '#' => toks.push(Token::Digit(false)),
301 '.' => toks.push(Token::Point),
302 ',' => toks.push(Token::Comma),
303 '%' => toks.push(Token::Percent),
304 '@' => toks.push(Token::At),
305 '$' | '-' | '+' | '(' | ')' | ':' | '!' | '^' | '&' | '\'' | '~' | '{' | '}' | '<'
306 | '>' | '=' | ' ' => toks.push(Token::Literal(c.to_string())),
307 '[' => {
308 let bracket: String = match chars[i..].iter().position(|&x| x == ']') {
309 Some(len) => chars[i..=i + len].iter().collect(),
310 None => chars[i..].iter().collect(),
311 };
312 return Err(Unsupported::new(
313 format!("{bracket}, a color, a condition, a locale or an elapsed time"),
314 "drop it",
315 ));
316 }
317 '?' => {
318 return Err(Unsupported::new(
319 "?, a digit's width",
320 "write 0 or # in its place",
321 ))
322 }
323 '/' => {
324 return Err(Unsupported::new(
325 "/, a fraction's bar",
326 "quote it, \"/\", to draw it as written",
327 ))
328 }
329 '_' => {
330 return Err(Unsupported::new(
331 "_, a character's width",
332 "drop it, or quote a space in its place",
333 ))
334 }
335 '*' => return Err(Unsupported::new("*, a fill", "drop it")),
336 c if c.is_ascii_alphabetic() => return Err(letter(&chars, i)),
337 c if c.is_ascii_digit() => {
338 return Err(Unsupported::new(
339 format!("{c}, a digit outside quotes"),
340 format!("quote it, \"{c}\", to draw it as written"),
341 ))
342 }
343 c if is_control(c) => return Err(control()),
344 c if c.is_ascii() => {
345 return Err(Unsupported::new(
346 format!("{c}, a character Excel draws only quoted"),
347 format!("quote it, \"{c}\", to draw it as written"),
348 ))
349 }
350 c => toks.push(Token::Literal(c.to_string())),
351 }
352 i += 1;
353 }
354 Ok(sections)
355}
356
357fn is_control(c: char) -> bool {
358 (c as u32) < 0x20 || c == '\u{7F}'
359}
360
361fn control() -> Unsupported {
362 Unsupported::new("a control character", "drop it")
363}
364
365fn letter(chars: &[char], i: usize) -> Unsupported {
368 let word: String = chars[i..].iter().take(7).collect();
369 if word.eq_ignore_ascii_case("general") {
370 return Unsupported::new(
371 "General inside a code",
372 "write the digits with 0 and # in its place",
373 );
374 }
375 let c = chars[i];
376 let sign = chars.get(i + 1).copied();
377 if (c == 'E' || c == 'e') && (sign == Some('+') || sign == Some('-')) {
378 let sign = sign.unwrap_or('+');
379 return Unsupported::new(
380 format!("{c}{sign}, a scientific exponent"),
381 "write the digits with 0 and # alone",
382 );
383 }
384 match c.to_ascii_lowercase() {
385 'y' | 'm' | 'd' | 'h' | 's' | 'a' | 'p' | 'b' | 'e' | 'g' => Unsupported::new(
386 format!("{c}, a date's or a time's letter"),
387 format!("quote it, \"{c}\", to draw it as written; this version draws no date or time"),
388 ),
389 _ => Unsupported::new(
390 format!("{c}, a letter outside quotes"),
391 format!("quote it, \"{c}\", to draw it as written"),
392 ),
393 }
394}
395
396fn number_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
398 if toks.contains(&Token::At) {
399 return Err(Unsupported::new(
400 "@ before the fourth section",
401 "put a text's section fourth, after zero's",
402 ));
403 }
404 if toks.iter().filter(|t| **t == Token::Point).count() > 1 {
405 return Err(Unsupported::new(
406 "a second point",
407 "keep one, or quote the other",
408 ));
409 }
410 let point = toks.iter().position(|t| *t == Token::Point);
411 let is_digit = |t: &Token| matches!(t, Token::Digit(_));
412 let before_point = |k: usize| point.map_or(true, |p| k < p);
413 let mut s = Section {
414 placeholders: toks.iter().any(is_digit),
415 ..Section::default()
416 };
417 let mut zeros: Vec<bool> = Vec::new();
418 for (k, t) in toks.iter().enumerate() {
419 match t {
420 Token::Digit(zero) => {
421 if before_point(k) {
422 s.integer += 1;
423 zeros.push(*zero);
424 } else {
425 s.decimals.push(*zero);
426 }
427 s.parts.push(Part::Digit(*zero));
428 }
429 Token::Point | Token::Percent if !s.placeholders => {
430 let what = if *t == Token::Point { "." } else { "%" };
431 return Err(Unsupported::new(
432 format!("{what} beside no digit placeholder"),
433 format!("write a digit with 0 or #, or quote it, \"{what}\""),
434 ));
435 }
436 Token::Point => s.parts.push(Part::Point),
437 Token::Percent => {
438 s.shift += 2;
439 s.parts.push(Part::Percent);
440 }
441 Token::Literal(text) => match s.parts.last_mut() {
442 Some(Part::Literal(last)) => last.push_str(text),
443 _ => s.parts.push(Part::Literal(text.clone())),
444 },
445 Token::Comma => {
446 if !toks[..k].iter().any(is_digit) {
447 return Err(Unsupported::new(
448 "a comma before any digit placeholder",
449 "drop it, or quote it, \",\"",
450 ));
451 }
452 match toks[k + 1..].iter().position(is_digit).map(|j| k + 1 + j) {
453 None => s.shift -= 3,
455 Some(j) if before_point(j) => s.grouping = true,
457 Some(_) if !before_point(k) => {
458 return Err(Unsupported::new(
459 "a comma among the decimals",
460 "drop it, or quote it, \",\"",
461 ))
462 }
463 Some(_) => {
464 return Err(Unsupported::new(
465 "a comma just before the point",
466 "move it after the last digit placeholder for thousandths, or drop it",
467 ))
468 }
469 }
470 }
471 Token::At => unreachable!("refused above"),
472 }
473 }
474 if s.decimals.len() > MOST_DECIMALS {
475 return Err(Unsupported::new(
476 format!("more than {MOST_DECIMALS} decimals"),
477 format!("keep {MOST_DECIMALS}, Excel's most"),
478 ));
479 }
480 s.min_integer = zeros
481 .iter()
482 .position(|zero| *zero)
483 .map_or(0, |first| zeros.len() - first);
484 Ok(s)
485}
486
487fn text_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
489 let mut s = Section::default();
490 for t in toks {
491 let what = match t {
492 Token::Literal(text) => {
493 match s.parts.last_mut() {
494 Some(Part::Literal(last)) => last.push_str(&text),
495 _ => s.parts.push(Part::Literal(text)),
496 }
497 continue;
498 }
499 Token::At => {
500 s.parts.push(Part::At);
501 continue;
502 }
503 Token::Digit(true) => "0",
504 Token::Digit(false) => "#",
505 Token::Point => ".",
506 Token::Comma => ",",
507 Token::Percent => "%",
508 };
509 return Err(Unsupported::new(
510 format!("{what} in a text's section"),
511 format!("quote it, \"{what}\", to draw it as written"),
512 ));
513 }
514 Ok(s)
515}
516
517impl Section {
518 fn text_of(&self, text: &str) -> String {
520 let mut out = String::new();
521 for p in &self.parts {
522 match p {
523 Part::Literal(l) => out.push_str(l),
524 Part::At => out.push_str(text),
525 _ => {}
526 }
527 }
528 out
529 }
530
531 fn draw(&self, magnitude: f64, out: &mut String) -> bool {
535 if !self.placeholders {
536 for p in &self.parts {
537 if let Part::Literal(l) = p {
538 out.push_str(l);
539 }
540 }
541 return false;
542 }
543 let (digits, point) = decimal(magnitude);
544 let (int, frac) = rounded(&digits, point + self.shift, self.decimals.len());
545 let zero = int.is_empty() && frac.iter().all(|&d| d == 0);
546 let mut kept = frac.len();
549 while kept > 0 && !self.decimals[kept - 1] && frac[kept - 1] == 0 {
550 kept -= 1;
551 }
552 let mut int = int;
553 if int.len() < self.min_integer {
554 let mut padded = vec![0; self.min_integer - int.len()];
555 padded.extend_from_slice(&int);
556 int = padded;
557 }
558 let len = int.len();
559 let push_digit = |j: usize, out: &mut String| {
560 out.push(char::from(b'0' + int[j]));
561 let from_right = len - 1 - j;
562 if self.grouping && from_right > 0 && from_right % 3 == 0 {
563 out.push(',');
564 }
565 };
566 let mut after_point = false;
567 let (mut k, mut d) = (0, 0);
568 for p in &self.parts {
569 match p {
570 Part::Digit(_) if !after_point => {
571 let r = self.integer - 1 - k;
575 if len > r {
576 let first = if k == 0 { 0 } else { len - 1 - r };
577 for j in first..=len - 1 - r {
578 push_digit(j, out);
579 }
580 }
581 k += 1;
582 }
583 Part::Digit(_) => {
584 if d < kept {
585 out.push(char::from(b'0' + frac[d]));
586 }
587 d += 1;
588 }
589 Part::Point => {
590 if self.integer == 0 {
591 for j in 0..len {
594 push_digit(j, out);
595 }
596 }
597 out.push('.');
598 after_point = true;
599 }
600 Part::Percent => out.push('%'),
601 Part::Literal(l) => out.push_str(l),
602 Part::At => {}
603 }
604 }
605 zero
606 }
607}
608
609fn decimal(magnitude: f64) -> (Vec<u8>, i32) {
619 if magnitude == 0.0 {
620 return (Vec::new(), 0);
621 }
622 let spelled = |text: String| -> (Vec<u8>, i32) {
623 let (mantissa, exponent) = text.split_once('e').expect("an exponent");
624 let digits: Vec<u8> = mantissa
625 .bytes()
626 .filter(u8::is_ascii_digit)
627 .map(|b| b - b'0')
628 .collect();
629 (
630 digits,
631 exponent.parse::<i32>().expect("a whole exponent") + 1,
632 )
633 };
634 let (mut digits, mut point) = spelled(format!("{magnitude:e}"));
635 if digits.len() == 16 && digits[15] == 5 && exactly(magnitude, &digits, point) {
636 digits.pop();
637 } else if digits.len() > 15 {
638 (digits, point) = spelled(format!("{magnitude:.14e}"));
639 }
640 while digits.last() == Some(&0) {
641 digits.pop();
642 }
643 (digits, point)
644}
645
646fn exactly(magnitude: f64, digits: &[u8], point: i32) -> bool {
651 let d = digits
652 .iter()
653 .fold(0u128, |acc, &x| acc * 10 + u128::from(x));
654 let p = point - digits.len() as i32;
655 if p >= 0 {
656 if p > 20 || magnitude.fract() != 0.0 || magnitude >= 1e38 {
657 return false;
658 }
659 magnitude as u128 == d * 10u128.pow(p as u32)
660 } else {
661 let q = (-p) as u32;
662 if q > 40 {
663 return false;
664 }
665 let five = 5u128.pow(q);
666 if d % five != 0 {
667 return false;
668 }
669 let scaled = magnitude * 2f64.powi(q as i32);
670 scaled.fract() == 0.0 && scaled < 1e38 && scaled as u128 == d / five
671 }
672}
673
674fn rounded(digits: &[u8], point: i32, decimals: usize) -> (Vec<u8>, Vec<u8>) {
679 let at = |j: i32| -> u8 {
680 if j < 0 {
681 0
682 } else {
683 digits.get(j as usize).copied().unwrap_or(0)
684 }
685 };
686 let keep = point + decimals as i32;
687 let mut kept: Vec<u8> = (0..keep.max(0)).map(at).collect();
688 if keep >= 0 && at(keep) >= 5 {
689 let mut i = kept.len();
690 loop {
691 if i == 0 {
692 kept.insert(0, 1);
693 break;
694 }
695 i -= 1;
696 if kept[i] == 9 {
697 kept[i] = 0;
698 } else {
699 kept[i] += 1;
700 break;
701 }
702 }
703 }
704 if kept.len() < decimals {
708 let mut padded = vec![0; decimals - kept.len()];
709 padded.extend_from_slice(&kept);
710 kept = padded;
711 }
712 let split = kept.len() - decimals;
713 let frac = kept[split..].to_vec();
714 let int = match kept[..split].iter().position(|&x| x != 0) {
715 Some(lead) => kept[lead..split].to_vec(),
716 None => Vec::new(),
717 };
718 (int, frac)
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 fn code(text: &str) -> Code {
726 Code::parse(text).unwrap_or_else(|u| panic!("{text}: {u:?}"))
727 }
728
729 fn drawn(text: &str, x: f64) -> String {
730 code(text).text(&Value::Number(x)).expect("a number draws")
731 }
732
733 fn refused(text: &str) -> Unsupported {
734 match Code::parse(text) {
735 Ok(c) => panic!("{text} was taken: {c:?}"),
736 Err(u) => u,
737 }
738 }
739
740 #[test]
741 fn a_number_draws_with_its_decimals() {
742 assert_eq!(drawn("0.00", 1.0), "1.00");
743 assert_eq!(drawn("0.00", 12.3), "12.30");
744 assert_eq!(drawn("0.00", 0.0), "0.00");
745 assert_eq!(drawn("0.00", -0.0), "0.00");
746 assert_eq!(drawn("0", 0.4), "0");
747 assert_eq!(drawn("#,##0.00", 1234.5), "1,234.50");
748 assert_eq!(drawn("#,##0", 1234567.0), "1,234,567");
749 assert_eq!(drawn("#,##0", 999.0), "999");
750 assert_eq!(drawn("0.00", 1e16), "10000000000000000.00");
751 }
752
753 #[test]
754 fn rounding_is_excel_s_half_away_from_zero_over_fifteen_digits() {
755 assert_eq!(drawn("0.00", 2.675), "2.68");
758 assert_eq!(drawn("0.00", 1.005), "1.01");
759 assert_eq!(drawn("0.00", 0.125), "0.13");
760 assert_eq!(drawn("0", 2.5), "3");
761 assert_eq!(drawn("0", 0.5), "1");
762 assert_eq!(drawn("0", -2.5), "-3");
763 assert_eq!(drawn("0.00", 9.995), "10.00");
764 assert_eq!(drawn("0.00", 0.005), "0.01");
765 assert_eq!(drawn("0.00", 0.0049), "0.00");
766 assert_eq!(drawn("0.00", 0.1 + 0.2), "0.30");
767 assert_eq!(drawn("0", 123456789012345678.0), "123456789012346000");
769 assert_eq!(drawn("0", 1234567890123455.0), "1234567890123450");
773 assert_eq!(drawn("0", 1234567890123465.0), "1234567890123460");
774 assert_eq!(drawn("0", 617283945061727.5), "617283945061727");
775 assert_eq!(drawn("0", 1234567890123455.5), "1234567890123460");
779 assert_eq!(
780 drawn("0.000000000000000", 0.2234567890123455),
781 "0.223456789012346"
782 );
783 assert_eq!(drawn("0", 1e20), "100000000000000000000");
784 assert_eq!(drawn("0.00", 5e-324), "0.00");
785 }
786
787 #[test]
788 fn sections_choose_by_sign_before_rounding() {
789 assert_eq!(drawn("0.00", -1.5), "-1.50");
790 assert_eq!(drawn("0.00", -0.001), "0.00");
793 assert_eq!(drawn("0", -0.4), "0");
794 assert_eq!(drawn("#,##0.00", -0.001), "0.00");
795 assert_eq!(drawn("0.00;(0.00)", -1.5), "(1.50)");
796 assert_eq!(drawn("0.00;(0.00)", -0.001), "(0.00)");
797 assert_eq!(drawn("0.00;(0.00)", 0.0), "0.00");
798 assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.0), "zero");
799 assert_eq!(drawn("0.00;(0.00);\"zero\"", -0.0), "zero");
800 assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.001), "0.00");
801 assert_eq!(drawn("▲0.00;▼0.00;=0.00", 1.5), "▲1.50");
802 assert_eq!(drawn("▲0.00;▼0.00;=0.00", -1.5), "▼1.50");
803 assert_eq!(drawn("▲0.00;▼0.00;=0.00", 0.0), "=0.00");
804 assert_eq!(drawn("0;;0", -3.0), "");
805 assert_eq!(drawn("0.00;-0.00", -2.0), "-2.00");
806 assert_eq!(drawn("\"x\"", -5.0), "-x");
807 }
808
809 #[test]
810 fn placeholders_draw_digits_zeros_and_nothing() {
811 assert_eq!(drawn("#", 0.0), "");
812 assert_eq!(drawn("#,###", 0.0), "");
813 assert_eq!(drawn("#.##", 0.5), ".5");
814 assert_eq!(drawn("0.#", 1.0), "1.");
815 assert_eq!(drawn("0.0#", 1.5), "1.5");
816 assert_eq!(drawn("0.0#", 1.25), "1.25");
817 assert_eq!(drawn("0.#0", 1.5), "1.50");
818 assert_eq!(drawn("000", 5.0), "005");
819 assert_eq!(drawn("#0#", 5.0), "05");
820 assert_eq!(drawn("000-00-0000", 123456789.0), "123-45-6789");
821 assert_eq!(drawn("000-00-0000", 5.0), "000-00-0005");
822 assert_eq!(drawn("0-0", 123.0), "12-3");
823 assert_eq!(drawn(".00", 12.5), "12.50");
824 assert_eq!(drawn(".00", 0.5), ".50");
825 }
826
827 #[test]
828 fn percents_and_thousandths_move_the_point_in_decimal() {
829 assert_eq!(drawn("0%", 0.5), "50%");
830 assert_eq!(drawn("0.00%", 0.12345), "12.35%");
831 assert_eq!(drawn("0.0%", 0.145), "14.5%");
832 assert_eq!(drawn("#,##0,", 1234567.891), "1,235");
833 assert_eq!(drawn("0.0,,", 12200000.0), "12.2");
834 assert_eq!(drawn("#,##0,\"K\"", 2500.0), "3K");
835 assert_eq!(drawn("0%%", 0.5), "5000%%");
836 }
837
838 #[test]
839 fn literals_draw_as_written() {
840 assert_eq!(drawn("\\$#,##0.00", 1234.5), "$1,234.50");
841 assert_eq!(drawn("$#,##0.00", -5.0), "-$5.00");
842 assert_eq!(drawn("0.00\" kg\"", 2.0), "2.00 kg");
843 assert_eq!(drawn("(000) 000-0000", 5551234567.0), "(555) 123-4567");
844 assert_eq!(drawn("#,##0 ;(#,##0)", 1234.0), "1,234 ");
845 assert_eq!(drawn("#,##0 ;(#,##0)", -1234.0), "(1,234)");
846 assert_eq!(drawn("\\€#,##0.00", 3.0), "€3.00");
847 }
848
849 #[test]
850 fn a_text_draws_by_the_fourth_section_alone() {
851 let c = code("0;-0;0;\"<\"@\">\"");
852 assert_eq!(c.text(&Value::Text("abc".into())), Some("<abc>".into()));
853 assert_eq!(code("0.00").text(&Value::Text("abc".into())), None);
854 assert_eq!(
855 code("0;-0;0;").text(&Value::Text("abc".into())),
856 Some(String::new())
857 );
858 assert_eq!(code("0.00").text(&Value::Bool(true)), None);
859 assert_eq!(code("0.00").text(&Value::Empty), None);
860 assert_eq!(code("0.00").text(&Value::Number(f64::INFINITY)), None);
861 }
862
863 #[test]
864 fn excel_s_built_ins_are_known_by_their_codes() {
865 assert_eq!(code("0.00").builtin(), Some(2));
866 assert_eq!(code("#,##0.00").builtin(), Some(4));
867 assert_eq!(code("#,##0 ;(#,##0)").builtin(), Some(37));
868 assert_eq!(code("0.000").builtin(), None);
869 assert_eq!(code("▲0.00;▼0.00;=0.00").builtin(), None);
870 assert_eq!(code("0.00"), code("0.00"));
871 assert_ne!(code("0.00"), code("0.00;-0.00"));
872 }
873
874 #[test]
875 fn what_this_version_does_not_draw_is_named() {
876 let named = |text: &str, word: &str| {
877 let u = refused(text);
878 assert!(u.what.contains(word), "{text}: {u:?}");
879 assert!(!u.fix.is_empty(), "{text}: {u:?}");
880 };
881 named("", "no code");
882 named("General", "General format");
883 named("general", "General format");
884 named("@", "Text format");
885 named("[Red]0.00", "[Red]");
886 named("0.00;[Red]-0.00", "color");
887 named("yyyy-mm-dd", "date");
888 named("0.00E+00", "E+");
889 named("# ?/?", "?");
890 named("0/0", "/");
891 named("_(0_)", "_");
892 named("* 0", "*");
893 named("General;-General", "General inside");
894 named("0 kg", "k");
895 named("0 1", "a digit");
896 named("0|", "|");
897 named("0.00.0", "second point");
898 named("0;0;0;@;0", "fifth section");
899 named("@;0", "@ before");
900 named("0;-0;0;0", "in a text's section");
901 named(",0", "before any digit");
902 named("0.0,0", "among the decimals");
903 named("0,.00", "just before the point");
904 named("\"x\".", "beside no digit");
905 named("\"open", "quote left open");
906 named("0\\", "backslash");
907 named(&format!("0.{}", "0".repeat(31)), "decimals");
908 named(&"0".repeat(256), "characters");
909 named("0\t", "control");
910 assert!(Code::parse(&format!("0.{}", "0".repeat(30))).is_ok());
911 }
912}