Rust · 44576 bytes Raw Blame History
1 //! Fortran FORMAT engine — complete implementation of all edit descriptors.
2 //!
3 //! Parses format strings like '(I5, F10.3, A, 2X, /, ES15.8)' into
4 //! descriptors and applies them to I/O values. Supports the full
5 //! Fortran standard set including repeat counts, group repeat,
6 //! unlimited repeat, scale factors, and all data/control descriptors.
7
8 /// A parsed format descriptor.
9 #[derive(Debug, Clone)]
10 pub enum FormatDesc {
11 // ---- Data edit descriptors ----
12 /// I: integer. Iw or Iw.m (w=width, m=minimum digits).
13 IntegerI {
14 width: usize,
15 min_digits: Option<usize>,
16 },
17 /// B: binary integer. Bw or Bw.m.
18 IntegerB {
19 width: usize,
20 min_digits: Option<usize>,
21 },
22 /// O: octal integer. Ow or Ow.m.
23 IntegerO {
24 width: usize,
25 min_digits: Option<usize>,
26 },
27 /// Z: hexadecimal integer. Zw or Zw.m.
28 IntegerZ {
29 width: usize,
30 min_digits: Option<usize>,
31 },
32 /// F: fixed-point real. Fw.d.
33 RealF { width: usize, decimals: usize },
34 /// E: exponential real. Ew.d or Ew.dEe.
35 RealE {
36 width: usize,
37 decimals: usize,
38 exp_width: Option<usize>,
39 },
40 /// EN: engineering notation. ENw.d or ENw.dEe.
41 RealEN {
42 width: usize,
43 decimals: usize,
44 exp_width: Option<usize>,
45 },
46 /// ES: scientific notation (1.0-9.999 mantissa). ESw.d or ESw.dEe.
47 RealES {
48 width: usize,
49 decimals: usize,
50 exp_width: Option<usize>,
51 },
52 /// EX: hexadecimal-significand real. EXw.d or EXw.dEe. (F2018)
53 RealEX {
54 width: usize,
55 decimals: usize,
56 exp_width: Option<usize>,
57 },
58 /// D: double-precision exponential. Dw.d (same as Ew.d with D exponent letter).
59 RealD { width: usize, decimals: usize },
60 /// G: generalized real. Gw.d or Gw.dEe. Chooses F or E format automatically.
61 RealG {
62 width: usize,
63 decimals: usize,
64 exp_width: Option<usize>,
65 },
66 /// L: logical. Lw.
67 Logical { width: usize },
68 /// A: character. A or Aw.
69 Character { width: Option<usize> },
70
71 // ---- Control edit descriptors ----
72 /// X: skip n positions. nX.
73 Skip { count: usize },
74 /// T: tab to absolute position. Tn.
75 TabTo { position: usize },
76 /// TL: tab left n positions. TLn.
77 TabLeft { count: usize },
78 /// TR: tab right n positions. TRn.
79 TabRight { count: usize },
80 /// /: new record (newline).
81 Newline,
82 /// :: stop processing if no more values.
83 Colon,
84 /// S, SP, SS: sign control.
85 Sign(SignMode),
86 /// BN, BZ: blank interpretation for input.
87 BlankMode(BlankInterpretation),
88 /// kP: scale factor.
89 ScaleFactor(i32),
90 /// RU, RD, RZ, RN, RC, RP: rounding mode (F2003).
91 RoundingMode(RoundMode),
92 /// DC, DP: decimal comma or point mode (F2003).
93 DecimalMode(DecimalSep),
94 /// DT: derived type I/O (F2003). Placeholder — requires user-defined I/O procedures.
95 DerivedType { type_name: String },
96
97 // ---- Character string descriptors ----
98 /// Literal string in format: 'text' or "text".
99 LiteralString(String),
100
101 // ---- Grouping ----
102 /// Repeated group: n(...).
103 Group {
104 repeat: usize,
105 descriptors: Vec<FormatDesc>,
106 },
107 /// Unlimited repeat: *(...).
108 UnlimitedRepeat { descriptors: Vec<FormatDesc> },
109 }
110
111 #[derive(Debug, Clone, Copy)]
112 pub enum SignMode {
113 /// S: processor-dependent (default).
114 Default,
115 /// SP: always show plus sign.
116 Plus,
117 /// SS: suppress plus sign.
118 Suppress,
119 }
120
121 #[derive(Debug, Clone, Copy)]
122 pub enum BlankInterpretation {
123 /// BN: blanks are null (ignored) in input.
124 Null,
125 /// BZ: blanks are zeros in input.
126 Zero,
127 }
128
129 #[derive(Debug, Clone, Copy)]
130 pub enum RoundMode {
131 Up, // RU
132 Down, // RD
133 Zero, // RZ
134 Nearest, // RN
135 Compatible, // RC
136 ProcessorDefined, // RP
137 }
138
139 #[derive(Debug, Clone, Copy)]
140 pub enum DecimalSep {
141 Comma, // DC
142 Point, // DP
143 }
144
145 /// Parse a Fortran format string (the part inside parentheses) into descriptors.
146 pub fn parse_format(fmt: &str) -> Vec<FormatDesc> {
147 let trimmed = fmt.trim();
148 // Strip outer parens if present.
149 let inner = if trimmed.starts_with('(') && trimmed.ends_with(')') {
150 &trimmed[1..trimmed.len() - 1]
151 } else {
152 trimmed
153 };
154 parse_format_list(inner)
155 }
156
157 fn parse_format_list(input: &str) -> Vec<FormatDesc> {
158 let mut result = Vec::new();
159 let mut chars = input.chars().peekable();
160
161 while chars.peek().is_some() {
162 skip_spaces(&mut chars);
163 if chars.peek().is_none() {
164 break;
165 }
166
167 // Check for comma separator.
168 if chars.peek() == Some(&',') {
169 chars.next();
170 continue;
171 }
172
173 // Check for negative sign (for scale factor: -kP).
174 let negative = if chars.peek() == Some(&'-') {
175 chars.next();
176 true
177 } else {
178 false
179 };
180
181 // Check for repeat count.
182 let repeat = parse_number(&mut chars);
183
184 skip_spaces(&mut chars);
185 if chars.peek().is_none() {
186 break;
187 }
188
189 let c = chars.peek().copied().unwrap_or(' ');
190
191 match c {
192 // ---- Group repeat ----
193 '(' => {
194 chars.next(); // consume '('
195 let inner = collect_until_matching_paren(&mut chars);
196 let descriptors = parse_format_list(&inner);
197 let n = repeat.unwrap_or(1);
198 if n == 0 {
199 // *(...) unlimited repeat — not representable with 0.
200 result.push(FormatDesc::UnlimitedRepeat { descriptors });
201 } else {
202 result.push(FormatDesc::Group {
203 repeat: n,
204 descriptors,
205 });
206 }
207 }
208
209 // ---- Literal strings ----
210 '\'' | '"' => {
211 let s = parse_string_literal(&mut chars, c);
212 for _ in 0..repeat.unwrap_or(1) {
213 result.push(FormatDesc::LiteralString(s.clone()));
214 }
215 }
216
217 // ---- Newline ----
218 '/' => {
219 chars.next();
220 for _ in 0..repeat.unwrap_or(1) {
221 result.push(FormatDesc::Newline);
222 }
223 }
224
225 // ---- Colon ----
226 ':' => {
227 chars.next();
228 result.push(FormatDesc::Colon);
229 }
230
231 // ---- Star (unlimited repeat) ----
232 '*' => {
233 chars.next();
234 if chars.peek() == Some(&'(') {
235 chars.next();
236 let inner = collect_until_matching_paren(&mut chars);
237 let descriptors = parse_format_list(&inner);
238 result.push(FormatDesc::UnlimitedRepeat { descriptors });
239 }
240 }
241
242 // ---- Edit descriptors ----
243 _ => {
244 let desc = parse_edit_descriptor(&mut chars, repeat, negative);
245 if let Some(d) = desc {
246 if let Some(n) = repeat {
247 if n > 1
248 && !matches!(d, FormatDesc::Skip { .. } | FormatDesc::ScaleFactor(_))
249 {
250 // Repeat count on a data descriptor: wrap in a group.
251 result.push(FormatDesc::Group {
252 repeat: n,
253 descriptors: vec![d],
254 });
255 } else {
256 result.push(d);
257 }
258 } else {
259 result.push(d);
260 }
261 }
262 }
263 }
264 }
265
266 result
267 }
268
269 fn parse_edit_descriptor(
270 chars: &mut std::iter::Peekable<std::str::Chars>,
271 repeat: Option<usize>,
272 negative: bool,
273 ) -> Option<FormatDesc> {
274 let letter = chars.next()?.to_ascii_uppercase();
275
276 match letter {
277 'I' => {
278 let w = parse_number(chars).unwrap_or(0);
279 let m = if chars.peek() == Some(&'.') {
280 chars.next();
281 parse_number(chars)
282 } else {
283 None
284 };
285 Some(FormatDesc::IntegerI {
286 width: w,
287 min_digits: m,
288 })
289 }
290 'B' if chars
291 .peek()
292 .map(|c| c.is_ascii_digit() || *c == '\'')
293 .unwrap_or(false) =>
294 {
295 // B followed by digit → binary integer format.
296 // B followed by quote → BOZ literal (not handled here).
297 let w = parse_number(chars).unwrap_or(0);
298 let m = if chars.peek() == Some(&'.') {
299 chars.next();
300 parse_number(chars)
301 } else {
302 None
303 };
304 Some(FormatDesc::IntegerB {
305 width: w,
306 min_digits: m,
307 })
308 }
309 'O' => {
310 let w = parse_number(chars).unwrap_or(0);
311 let m = if chars.peek() == Some(&'.') {
312 chars.next();
313 parse_number(chars)
314 } else {
315 None
316 };
317 Some(FormatDesc::IntegerO {
318 width: w,
319 min_digits: m,
320 })
321 }
322 'Z' => {
323 let w = parse_number(chars).unwrap_or(0);
324 let m = if chars.peek() == Some(&'.') {
325 chars.next();
326 parse_number(chars)
327 } else {
328 None
329 };
330 Some(FormatDesc::IntegerZ {
331 width: w,
332 min_digits: m,
333 })
334 }
335 'F' => {
336 let w = parse_number(chars).unwrap_or(0);
337 let d = if chars.peek() == Some(&'.') {
338 chars.next();
339 parse_number(chars).unwrap_or(0)
340 } else {
341 0
342 };
343 Some(FormatDesc::RealF {
344 width: w,
345 decimals: d,
346 })
347 }
348 'E' => {
349 // Check for EN, ES, EX.
350 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
351 match next {
352 'N' => {
353 chars.next();
354 parse_real_desc(chars, |w, d, e| FormatDesc::RealEN {
355 width: w,
356 decimals: d,
357 exp_width: e,
358 })
359 }
360 'S' => {
361 chars.next();
362 parse_real_desc(chars, |w, d, e| FormatDesc::RealES {
363 width: w,
364 decimals: d,
365 exp_width: e,
366 })
367 }
368 'X' => {
369 chars.next();
370 parse_real_desc(chars, |w, d, e| FormatDesc::RealEX {
371 width: w,
372 decimals: d,
373 exp_width: e,
374 })
375 }
376 _ => parse_real_desc(chars, |w, d, e| FormatDesc::RealE {
377 width: w,
378 decimals: d,
379 exp_width: e,
380 }),
381 }
382 }
383 'D' => {
384 // DC/DP (decimal mode) vs DT (derived type) vs Dw.d (real format).
385 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
386 match next {
387 'C' => {
388 chars.next();
389 Some(FormatDesc::DecimalMode(DecimalSep::Comma))
390 }
391 'P' => {
392 chars.next();
393 Some(FormatDesc::DecimalMode(DecimalSep::Point))
394 }
395 'T' => {
396 chars.next();
397 // DT optionally followed by 'typename'.
398 let name = if chars.peek() == Some(&'\'') || chars.peek() == Some(&'"') {
399 let q = *chars.peek().unwrap();
400 parse_string_literal(chars, q)
401 } else {
402 String::new()
403 };
404 Some(FormatDesc::DerivedType { type_name: name })
405 }
406 _ => {
407 let w = parse_number(chars).unwrap_or(0);
408 let d = if chars.peek() == Some(&'.') {
409 chars.next();
410 parse_number(chars).unwrap_or(0)
411 } else {
412 0
413 };
414 Some(FormatDesc::RealD {
415 width: w,
416 decimals: d,
417 })
418 }
419 }
420 }
421 'G' => parse_real_desc(chars, |w, d, e| FormatDesc::RealG {
422 width: w,
423 decimals: d,
424 exp_width: e,
425 }),
426 'L' => {
427 let w = parse_number(chars).unwrap_or(1);
428 Some(FormatDesc::Logical { width: w })
429 }
430 'A' => {
431 let w = parse_number(chars);
432 Some(FormatDesc::Character { width: w })
433 }
434 'X' => Some(FormatDesc::Skip {
435 count: repeat.unwrap_or(1),
436 }),
437 'T' => {
438 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
439 match next {
440 'L' => {
441 chars.next();
442 let n = parse_number(chars).unwrap_or(1);
443 Some(FormatDesc::TabLeft { count: n })
444 }
445 'R' => {
446 chars.next();
447 let n = parse_number(chars).unwrap_or(1);
448 Some(FormatDesc::TabRight { count: n })
449 }
450 _ => {
451 let n = parse_number(chars).unwrap_or(1);
452 Some(FormatDesc::TabTo { position: n })
453 }
454 }
455 }
456 'S' => {
457 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
458 match next {
459 'P' => {
460 chars.next();
461 Some(FormatDesc::Sign(SignMode::Plus))
462 }
463 'S' => {
464 chars.next();
465 Some(FormatDesc::Sign(SignMode::Suppress))
466 }
467 _ => Some(FormatDesc::Sign(SignMode::Default)),
468 }
469 }
470 'P' => {
471 // kP — repeat is the scale factor magnitude, sign from negative flag.
472 let k = repeat.unwrap_or(0) as i32;
473 Some(FormatDesc::ScaleFactor(if negative { -k } else { k }))
474 }
475 'R' => {
476 // Rounding modes: RU, RD, RZ, RN, RC, RP.
477 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
478 let mode = match next {
479 'U' => {
480 chars.next();
481 Some(RoundMode::Up)
482 }
483 'D' => {
484 chars.next();
485 Some(RoundMode::Down)
486 }
487 'Z' => {
488 chars.next();
489 Some(RoundMode::Zero)
490 }
491 'N' => {
492 chars.next();
493 Some(RoundMode::Nearest)
494 }
495 'C' => {
496 chars.next();
497 Some(RoundMode::Compatible)
498 }
499 'P' => {
500 chars.next();
501 Some(RoundMode::ProcessorDefined)
502 }
503 _ => None,
504 };
505 mode.map(FormatDesc::RoundingMode)
506 }
507 'B' => {
508 // BN or BZ.
509 let next = chars.peek().copied().unwrap_or(' ').to_ascii_uppercase();
510 match next {
511 'N' => {
512 chars.next();
513 Some(FormatDesc::BlankMode(BlankInterpretation::Null))
514 }
515 'Z' => {
516 chars.next();
517 Some(FormatDesc::BlankMode(BlankInterpretation::Zero))
518 }
519 _ => None,
520 }
521 }
522 _ => None, // unknown descriptor
523 }
524 }
525
526 fn parse_real_desc(
527 chars: &mut std::iter::Peekable<std::str::Chars>,
528 constructor: impl Fn(usize, usize, Option<usize>) -> FormatDesc,
529 ) -> Option<FormatDesc> {
530 let w = parse_number(chars).unwrap_or(0);
531 let d = if chars.peek() == Some(&'.') {
532 chars.next();
533 parse_number(chars).unwrap_or(0)
534 } else {
535 0
536 };
537 let e = if chars
538 .peek()
539 .map(|c| c.eq_ignore_ascii_case(&'E'))
540 .unwrap_or(false)
541 {
542 chars.next();
543 parse_number(chars)
544 } else {
545 None
546 };
547 Some(constructor(w, d, e))
548 }
549
550 // ---- Format application (output) ----
551
552 /// An I/O value to be formatted.
553 pub enum IoValue {
554 Integer(i128),
555 Real(f64),
556 Logical(bool),
557 Character(Vec<u8>),
558 }
559
560 /// Format engine state for applying descriptors to values.
561 pub struct FormatEngine {
562 descriptors: Vec<FormatDesc>,
563 sign_mode: SignMode,
564 scale_factor: i32,
565 round_mode: RoundMode,
566 decimal_sep: DecimalSep,
567 }
568
569 impl FormatEngine {
570 pub fn new(descriptors: Vec<FormatDesc>) -> Self {
571 Self {
572 descriptors,
573 sign_mode: SignMode::Default,
574 scale_factor: 0,
575 round_mode: RoundMode::Compatible,
576 decimal_sep: DecimalSep::Point,
577 }
578 }
579
580 /// Format a list of values according to the descriptors, producing an output string.
581 pub fn format_values(&mut self, values: &[IoValue]) -> String {
582 let mut output = String::new();
583 let mut val_idx = 0;
584 self.apply_descriptors(&self.descriptors.clone(), values, &mut val_idx, &mut output);
585 output
586 }
587
588 fn apply_descriptors(
589 &mut self,
590 descs: &[FormatDesc],
591 values: &[IoValue],
592 val_idx: &mut usize,
593 output: &mut String,
594 ) {
595 for desc in descs {
596 match desc {
597 // ---- Control descriptors ----
598 FormatDesc::Skip { count } => {
599 for _ in 0..*count {
600 output.push(' ');
601 }
602 }
603 FormatDesc::Newline => {
604 output.push('\n');
605 }
606 FormatDesc::Colon => {
607 if *val_idx >= values.len() {
608 return;
609 }
610 }
611 FormatDesc::Sign(mode) => {
612 self.sign_mode = *mode;
613 }
614 FormatDesc::ScaleFactor(k) => {
615 self.scale_factor = *k;
616 }
617 FormatDesc::BlankMode(_) => {} // input only
618 FormatDesc::RoundingMode(mode) => {
619 self.round_mode = *mode;
620 }
621 FormatDesc::DecimalMode(sep) => {
622 self.decimal_sep = *sep;
623 }
624 FormatDesc::DerivedType { .. } => {} // requires user-defined I/O — no-op for now
625 FormatDesc::TabTo { position } => {
626 let cur_col = output.lines().last().map(|l| l.len()).unwrap_or(0);
627 if *position > cur_col + 1 {
628 for _ in 0..(*position - cur_col - 1) {
629 output.push(' ');
630 }
631 }
632 }
633 FormatDesc::TabLeft { count } => {
634 // Truncate output by `count` characters (simplified).
635 let new_len = output.len().saturating_sub(*count);
636 output.truncate(new_len);
637 }
638 FormatDesc::TabRight { count } => {
639 for _ in 0..*count {
640 output.push(' ');
641 }
642 }
643 FormatDesc::LiteralString(s) => {
644 output.push_str(s);
645 }
646
647 // ---- Group repeat ----
648 FormatDesc::Group {
649 repeat,
650 descriptors,
651 } => {
652 for _ in 0..*repeat {
653 self.apply_descriptors(descriptors, values, val_idx, output);
654 }
655 }
656 FormatDesc::UnlimitedRepeat { descriptors } => {
657 while *val_idx < values.len() {
658 self.apply_descriptors(descriptors, values, val_idx, output);
659 }
660 }
661
662 // ---- Data descriptors ----
663 _ => {
664 if *val_idx >= values.len() {
665 return;
666 }
667 let val = &values[*val_idx];
668 *val_idx += 1;
669 let formatted = self.format_value(desc, val);
670 output.push_str(&formatted);
671 }
672 }
673 }
674 }
675
676 fn format_value(&self, desc: &FormatDesc, val: &IoValue) -> String {
677 match (desc, val) {
678 // ---- Integer ----
679 (FormatDesc::IntegerI { width, min_digits }, IoValue::Integer(v)) => {
680 let s = if let Some(m) = min_digits {
681 let abs_s = format!("{}", v.unsigned_abs());
682 let padded = format!("{:0>width$}", abs_s, width = *m);
683 if *v < 0 {
684 format!("-{}", padded)
685 } else {
686 self.apply_sign(&padded, *v >= 0)
687 }
688 } else {
689 self.apply_sign(&format!("{}", v.unsigned_abs()), *v >= 0)
690 };
691 format!("{:>width$}", s, width = *width)
692 }
693 (FormatDesc::IntegerB { width, min_digits }, IoValue::Integer(v)) => {
694 let s = format_radix_integer(*v, *min_digits, 2);
695 format!("{:>width$}", s, width = *width)
696 }
697 (FormatDesc::IntegerO { width, min_digits }, IoValue::Integer(v)) => {
698 let s = format_radix_integer(*v, *min_digits, 8);
699 format!("{:>width$}", s, width = *width)
700 }
701 (FormatDesc::IntegerZ { width, min_digits }, IoValue::Integer(v)) => {
702 let s = format_radix_integer(*v, *min_digits, 16);
703 format!("{:>width$}", s, width = *width)
704 }
705
706 // ---- Real ----
707 (FormatDesc::RealF { width, decimals }, IoValue::Real(v)) => {
708 // kP scale factor: F format multiplies value by 10^k.
709 let scaled = *v * 10f64.powi(self.scale_factor);
710 let rounded = self.apply_rounding(scaled, *decimals);
711 let s = self.format_fixed(rounded, *decimals);
712 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
713 }
714 (
715 FormatDesc::RealE {
716 width,
717 decimals,
718 exp_width,
719 },
720 IoValue::Real(v),
721 ) => {
722 let s = self.format_e_style(*v, *decimals, *exp_width, 'E');
723 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
724 }
725 (
726 FormatDesc::RealES {
727 width,
728 decimals,
729 exp_width,
730 },
731 IoValue::Real(v),
732 ) => {
733 // Scientific: mantissa in [1.0, 10.0). Equivalent to 1P,E.
734 let s = self.format_es_style(*v, *decimals, *exp_width);
735 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
736 }
737 (
738 FormatDesc::RealEN {
739 width, decimals, ..
740 },
741 IoValue::Real(v),
742 ) => {
743 // Engineering: exponent is multiple of 3.
744 let (mantissa, exp) = to_engineering(*v);
745 let rounded = self.apply_rounding(mantissa, *decimals);
746 let s = format!("{:.*}E{:+03}", *decimals, rounded, exp);
747 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
748 }
749 (FormatDesc::RealD { width, decimals }, IoValue::Real(v)) => {
750 let s = self.format_e_style(*v, *decimals, None, 'D');
751 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
752 }
753 (
754 FormatDesc::RealG {
755 width,
756 decimals,
757 exp_width,
758 },
759 IoValue::Real(v),
760 ) => {
761 // G format: use F if magnitude fits, else E.
762 let abs_v = v.abs();
763 if abs_v == 0.0 || (abs_v >= 0.1 && abs_v < 10f64.powi(*decimals as i32)) {
764 let rounded = self.apply_rounding(*v, *decimals);
765 let s = self.format_fixed(rounded, *decimals);
766 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
767 } else {
768 let s = self.format_e_style(*v, *decimals, *exp_width, 'E');
769 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
770 }
771 }
772 (
773 FormatDesc::RealEX {
774 width, decimals, ..
775 },
776 IoValue::Real(v),
777 ) => {
778 // Hex-significand: use %a-like format. Rust doesn't have this natively.
779 let s = format!("{:.*E}", *decimals, v); // fallback to E format
780 self.apply_decimal_sep(&format!("{:>width$}", s, width = *width))
781 }
782
783 // ---- Logical ----
784 (FormatDesc::Logical { width }, IoValue::Logical(v)) => {
785 let s = if *v { "T" } else { "F" };
786 format!("{:>width$}", s, width = *width)
787 }
788
789 // ---- Character ----
790 (FormatDesc::Character { width }, IoValue::Character(bytes)) => {
791 let s = String::from_utf8_lossy(bytes);
792 if let Some(w) = width {
793 if *w > s.len() {
794 format!("{:>width$}", s, width = *w)
795 } else {
796 s[..*w].to_string()
797 }
798 } else {
799 s.into_owned()
800 }
801 }
802
803 // Type mismatch: format as-is.
804 (_, IoValue::Integer(v)) => format!("{}", v),
805 (_, IoValue::Real(v)) => format!("{}", v),
806 (_, IoValue::Logical(v)) => (if *v { "T" } else { "F" }).into(),
807 (_, IoValue::Character(b)) => String::from_utf8_lossy(b).into_owned(),
808 }
809 }
810
811 fn apply_sign(&self, abs_str: &str, is_positive: bool) -> String {
812 if is_positive {
813 match self.sign_mode {
814 SignMode::Plus => format!("+{}", abs_str),
815 _ => abs_str.to_string(),
816 }
817 } else {
818 format!("-{}", abs_str)
819 }
820 }
821
822 /// Apply rounding mode to a value at the given number of decimal places.
823 fn apply_rounding(&self, v: f64, decimals: usize) -> f64 {
824 let factor = 10f64.powi(decimals as i32);
825 let scaled = v * factor;
826 match self.round_mode {
827 RoundMode::Up => scaled.ceil() / factor,
828 RoundMode::Down => scaled.floor() / factor,
829 RoundMode::Zero => scaled.trunc() / factor,
830 RoundMode::Nearest => {
831 // IEEE 754 round-to-nearest-even (banker's rounding).
832 let rounded = scaled.round();
833 // Check for exact halfway: round to even.
834 if (scaled - scaled.floor() - 0.5).abs() < 1e-15 {
835 if rounded as i64 % 2 != 0 {
836 (rounded - scaled.signum()) / factor
837 } else {
838 rounded / factor
839 }
840 } else {
841 rounded / factor
842 }
843 }
844 RoundMode::Compatible => {
845 // Round half away from zero (standard mathematical rounding).
846 (scaled + 0.5 * scaled.signum()).trunc() / factor
847 }
848 RoundMode::ProcessorDefined => {
849 // Use Rust's default (round-half-to-even).
850 scaled.round() / factor
851 }
852 }
853 }
854
855 /// Format a fixed-point number (for F and G-as-F).
856 fn format_fixed(&self, v: f64, decimals: usize) -> String {
857 format!("{:.*}", decimals, v)
858 }
859
860 /// Format in E/D style with scale factor applied.
861 ///
862 /// Fortran kP with E format: the mantissa is multiplied by 10^k,
863 /// and the exponent is decreased by k. With 0P (default), the mantissa
864 /// is in [0.1, 1.0) — Fortran's convention, not C's.
865 fn format_e_style(
866 &self,
867 v: f64,
868 decimals: usize,
869 exp_width: Option<usize>,
870 exp_char: char,
871 ) -> String {
872 if v == 0.0 {
873 let ew = exp_width.unwrap_or(2);
874 return format!(
875 "0.{:0>d$}{}{:+0ew$}",
876 "",
877 exp_char,
878 0,
879 d = decimals,
880 ew = ew + 1
881 );
882 }
883
884 let abs_v = v.abs();
885 let base_exp = abs_v.log10().floor() as i32;
886 // Fortran default (0P): mantissa in [0.1, 1.0), so exponent = base_exp + 1.
887 let fort_exp = base_exp + 1 - self.scale_factor;
888 let mantissa = abs_v / 10f64.powi(base_exp + 1 - self.scale_factor);
889 let rounded = self.apply_rounding(mantissa, decimals);
890
891 let ew = exp_width.unwrap_or(2);
892 let sign = if v < 0.0 {
893 "-"
894 } else if matches!(self.sign_mode, SignMode::Plus) {
895 "+"
896 } else {
897 ""
898 };
899 format!(
900 "{}{:.*}{}{:+0ew$}",
901 sign,
902 decimals,
903 rounded,
904 exp_char,
905 fort_exp,
906 ew = ew + 1
907 )
908 }
909
910 /// Format in ES style (scientific): mantissa in [1.0, 10.0).
911 fn format_es_style(&self, v: f64, decimals: usize, exp_width: Option<usize>) -> String {
912 if v == 0.0 {
913 let ew = exp_width.unwrap_or(2);
914 return format!("0.{:0>d$}E{:+0ew$}", "", 0, d = decimals, ew = ew + 1);
915 }
916
917 let abs_v = v.abs();
918 let base_exp = abs_v.log10().floor() as i32;
919 let mantissa = abs_v / 10f64.powi(base_exp);
920 let rounded = self.apply_rounding(mantissa, decimals);
921
922 let ew = exp_width.unwrap_or(2);
923 let sign = if v < 0.0 {
924 "-"
925 } else if matches!(self.sign_mode, SignMode::Plus) {
926 "+"
927 } else {
928 ""
929 };
930 format!(
931 "{}{:.*}E{:+0ew$}",
932 sign,
933 decimals,
934 rounded,
935 base_exp,
936 ew = ew + 1
937 )
938 }
939
940 /// Replace '.' with ',' when decimal mode is DC (comma).
941 fn apply_decimal_sep(&self, s: &str) -> String {
942 match self.decimal_sep {
943 DecimalSep::Point => s.to_string(),
944 DecimalSep::Comma => s.replace('.', ","),
945 }
946 }
947 }
948
949 fn format_radix_integer(value: i128, min_digits: Option<usize>, radix: u32) -> String {
950 let digits = match radix {
951 2 => format!("{:b}", value.unsigned_abs()),
952 8 => format!("{:o}", value.unsigned_abs()),
953 16 => format!("{:X}", value.unsigned_abs()),
954 _ => unreachable!("unsupported radix"),
955 };
956 let padded = if let Some(min_digits) = min_digits {
957 format!("{:0>width$}", digits, width = min_digits)
958 } else {
959 digits
960 };
961 if value < 0 {
962 format!("-{}", padded)
963 } else {
964 padded
965 }
966 }
967
968 // ---- Helpers ----
969
970 fn to_engineering(v: f64) -> (f64, i32) {
971 if v == 0.0 {
972 return (0.0, 0);
973 }
974 let exp = (v.abs().log10().floor() as i32) / 3 * 3;
975 let mantissa = v / 10f64.powi(exp);
976 (mantissa, exp)
977 }
978
979 fn skip_spaces(chars: &mut std::iter::Peekable<std::str::Chars>) {
980 while chars.peek() == Some(&' ') {
981 chars.next();
982 }
983 }
984
985 fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<usize> {
986 let mut digits = String::new();
987 while let Some(&c) = chars.peek() {
988 if c.is_ascii_digit() {
989 digits.push(c);
990 chars.next();
991 } else {
992 break;
993 }
994 }
995 if digits.is_empty() {
996 None
997 } else {
998 digits.parse().ok()
999 }
1000 }
1001
1002 fn parse_string_literal(chars: &mut std::iter::Peekable<std::str::Chars>, quote: char) -> String {
1003 chars.next(); // consume opening quote
1004 let mut s = String::new();
1005 while let Some(&c) = chars.peek() {
1006 chars.next();
1007 if c == quote {
1008 // Check for doubled quote (escape).
1009 if chars.peek() == Some(&quote) {
1010 chars.next();
1011 s.push(quote);
1012 } else {
1013 break;
1014 }
1015 } else {
1016 s.push(c);
1017 }
1018 }
1019 s
1020 }
1021
1022 fn collect_until_matching_paren(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
1023 let mut depth = 1;
1024 let mut inner = String::new();
1025 while let Some(&c) = chars.peek() {
1026 chars.next();
1027 if c == '(' {
1028 depth += 1;
1029 }
1030 if c == ')' {
1031 depth -= 1;
1032 if depth == 0 {
1033 break;
1034 }
1035 }
1036 inner.push(c);
1037 }
1038 inner
1039 }
1040
1041 #[cfg(test)]
1042 mod tests {
1043 use super::*;
1044
1045 #[test]
1046 fn parse_simple_format() {
1047 let descs = parse_format("(I5, F10.3, A)");
1048 assert_eq!(descs.len(), 3);
1049 assert!(matches!(
1050 descs[0],
1051 FormatDesc::IntegerI {
1052 width: 5,
1053 min_digits: None
1054 }
1055 ));
1056 assert!(matches!(
1057 descs[1],
1058 FormatDesc::RealF {
1059 width: 10,
1060 decimals: 3
1061 }
1062 ));
1063 assert!(matches!(descs[2], FormatDesc::Character { width: None }));
1064 }
1065
1066 #[test]
1067 fn parse_with_repeat() {
1068 // 3I5 means "repeat I5 three times" — wrapped in a Group.
1069 let descs = parse_format("(3I5)");
1070 assert_eq!(descs.len(), 1);
1071 assert!(matches!(descs[0], FormatDesc::Group { repeat: 3, .. }));
1072 }
1073
1074 #[test]
1075 fn parse_control_descriptors() {
1076 let descs = parse_format("(2X, /, SP, T10)");
1077 assert!(matches!(descs[0], FormatDesc::Skip { count: 2 }));
1078 assert!(matches!(descs[1], FormatDesc::Newline));
1079 assert!(matches!(descs[2], FormatDesc::Sign(SignMode::Plus)));
1080 assert!(matches!(descs[3], FormatDesc::TabTo { position: 10 }));
1081 }
1082
1083 #[test]
1084 fn parse_string_literal() {
1085 let descs = parse_format("('hello', A)");
1086 assert_eq!(descs.len(), 2);
1087 if let FormatDesc::LiteralString(s) = &descs[0] {
1088 assert_eq!(s, "hello");
1089 } else {
1090 panic!("expected literal");
1091 }
1092 }
1093
1094 #[test]
1095 fn parse_es_en_format() {
1096 let descs = parse_format("(ES15.8, EN12.3)");
1097 assert!(matches!(
1098 descs[0],
1099 FormatDesc::RealES {
1100 width: 15,
1101 decimals: 8,
1102 ..
1103 }
1104 ));
1105 assert!(matches!(
1106 descs[1],
1107 FormatDesc::RealEN {
1108 width: 12,
1109 decimals: 3,
1110 ..
1111 }
1112 ));
1113 }
1114
1115 #[test]
1116 fn format_integer() {
1117 let descs = parse_format("(I5)");
1118 let mut engine = FormatEngine::new(descs);
1119 let out = engine.format_values(&[IoValue::Integer(42)]);
1120 assert_eq!(out, " 42");
1121 }
1122
1123 #[test]
1124 fn format_integer_with_min_digits() {
1125 let descs = parse_format("(I5.3)");
1126 let mut engine = FormatEngine::new(descs);
1127 let out = engine.format_values(&[IoValue::Integer(7)]);
1128 assert_eq!(out, " 007");
1129 }
1130
1131 #[test]
1132 fn format_real_f() {
1133 let descs = parse_format("(F8.3)");
1134 let mut engine = FormatEngine::new(descs);
1135 let out = engine.format_values(&[IoValue::Real(3.14159)]);
1136 assert_eq!(out, " 3.142");
1137 }
1138
1139 #[test]
1140 fn format_logical() {
1141 let descs = parse_format("(L3)");
1142 let mut engine = FormatEngine::new(descs);
1143 let out = engine.format_values(&[IoValue::Logical(true)]);
1144 assert_eq!(out, " T");
1145 }
1146
1147 #[test]
1148 fn format_character() {
1149 let descs = parse_format("(A5)");
1150 let mut engine = FormatEngine::new(descs);
1151 let out = engine.format_values(&[IoValue::Character(b"hi".to_vec())]);
1152 assert_eq!(out, " hi");
1153 }
1154
1155 #[test]
1156 fn format_mixed() {
1157 let descs = parse_format("('Count: ', I4)");
1158 let mut engine = FormatEngine::new(descs);
1159 let out = engine.format_values(&[IoValue::Integer(42)]);
1160 assert_eq!(out, "Count: 42");
1161 }
1162
1163 #[test]
1164 fn format_with_newline() {
1165 let descs = parse_format("(I3, /, I3)");
1166 let mut engine = FormatEngine::new(descs);
1167 let out = engine.format_values(&[IoValue::Integer(1), IoValue::Integer(2)]);
1168 assert_eq!(out, " 1\n 2");
1169 }
1170
1171 #[test]
1172 fn format_skip() {
1173 let descs = parse_format("(I3, 3X, I3)");
1174 let mut engine = FormatEngine::new(descs);
1175 let out = engine.format_values(&[IoValue::Integer(1), IoValue::Integer(2)]);
1176 assert_eq!(out, " 1 2");
1177 }
1178
1179 #[test]
1180 fn format_sign_plus() {
1181 let descs = parse_format("(SP, I5)");
1182 let mut engine = FormatEngine::new(descs);
1183 let out = engine.format_values(&[IoValue::Integer(42)]);
1184 assert_eq!(out, " +42");
1185 }
1186
1187 #[test]
1188 fn format_hex_integer() {
1189 let descs = parse_format("(Z4)");
1190 let mut engine = FormatEngine::new(descs);
1191 let out = engine.format_values(&[IoValue::Integer(255)]);
1192 assert_eq!(out, " FF");
1193 }
1194
1195 #[test]
1196 fn format_octal_integer() {
1197 let descs = parse_format("(O6)");
1198 let mut engine = FormatEngine::new(descs);
1199 let out = engine.format_values(&[IoValue::Integer(255)]);
1200 assert_eq!(out, " 377");
1201 }
1202
1203 #[test]
1204 fn format_octal_integer_with_min_digits() {
1205 let descs = parse_format("(O4.4)");
1206 let mut engine = FormatEngine::new(descs);
1207 let out = engine.format_values(&[IoValue::Integer(18)]);
1208 assert_eq!(out, "0022");
1209 }
1210
1211 #[test]
1212 fn format_colon_stops_early() {
1213 let descs = parse_format("(I3, :, ', ', I3)");
1214 let mut engine = FormatEngine::new(descs);
1215 // Only one value — colon stops before the comma-space-I3.
1216 let out = engine.format_values(&[IoValue::Integer(42)]);
1217 assert_eq!(out, " 42");
1218 }
1219
1220 #[test]
1221 fn format_unlimited_repeat() {
1222 let descs = parse_format("(*(I3, ','))");
1223 let mut engine = FormatEngine::new(descs);
1224 let out = engine.format_values(&[
1225 IoValue::Integer(1),
1226 IoValue::Integer(2),
1227 IoValue::Integer(3),
1228 ]);
1229 assert_eq!(out, " 1, 2, 3,");
1230 }
1231
1232 #[test]
1233 fn parse_dc_dp_decimal_mode() {
1234 let descs = parse_format("(DC, F8.3, DP, F8.3)");
1235 assert_eq!(descs.len(), 4);
1236 assert!(matches!(
1237 descs[0],
1238 FormatDesc::DecimalMode(DecimalSep::Comma)
1239 ));
1240 assert!(matches!(
1241 descs[2],
1242 FormatDesc::DecimalMode(DecimalSep::Point)
1243 ));
1244 }
1245
1246 #[test]
1247 fn parse_dt_derived_type() {
1248 let descs = parse_format("(DT'mytype')");
1249 assert_eq!(descs.len(), 1);
1250 if let FormatDesc::DerivedType { ref type_name } = descs[0] {
1251 assert_eq!(type_name, "mytype");
1252 } else {
1253 panic!("expected DerivedType, got {:?}", descs[0]);
1254 }
1255 }
1256
1257 #[test]
1258 fn parse_dt_no_name() {
1259 let descs = parse_format("(DT)");
1260 assert_eq!(descs.len(), 1);
1261 if let FormatDesc::DerivedType { ref type_name } = descs[0] {
1262 assert_eq!(type_name, "");
1263 } else {
1264 panic!("expected DerivedType");
1265 }
1266 }
1267
1268 #[test]
1269 fn parse_rounding_modes() {
1270 let descs = parse_format("(RU, F8.3, RD, F8.3, RZ, F8.3, RN, F8.3, RC, F8.3, RP, F8.3)");
1271 assert!(matches!(descs[0], FormatDesc::RoundingMode(RoundMode::Up)));
1272 assert!(matches!(
1273 descs[2],
1274 FormatDesc::RoundingMode(RoundMode::Down)
1275 ));
1276 assert!(matches!(
1277 descs[4],
1278 FormatDesc::RoundingMode(RoundMode::Zero)
1279 ));
1280 assert!(matches!(
1281 descs[6],
1282 FormatDesc::RoundingMode(RoundMode::Nearest)
1283 ));
1284 assert!(matches!(
1285 descs[8],
1286 FormatDesc::RoundingMode(RoundMode::Compatible)
1287 ));
1288 assert!(matches!(
1289 descs[10],
1290 FormatDesc::RoundingMode(RoundMode::ProcessorDefined)
1291 ));
1292 }
1293
1294 #[test]
1295 fn parse_negative_scale_factor() {
1296 let descs = parse_format("(-2P, E15.8)");
1297 assert!(matches!(descs[0], FormatDesc::ScaleFactor(-2)));
1298 }
1299
1300 #[test]
1301 fn parse_positive_scale_factor() {
1302 let descs = parse_format("(3P, E15.8)");
1303 assert!(matches!(descs[0], FormatDesc::ScaleFactor(3)));
1304 }
1305
1306 #[test]
1307 fn format_decimal_comma() {
1308 let descs = parse_format("(DC, F8.3)");
1309 let mut engine = FormatEngine::new(descs);
1310 let out = engine.format_values(&[IoValue::Real(3.14)]);
1311 assert!(out.contains(','), "expected comma in output: {}", out);
1312 assert!(!out.contains('.'), "expected no dot in output: {}", out);
1313 }
1314
1315 #[test]
1316 fn format_rounding_up() {
1317 let descs = parse_format("(RU, F6.2)");
1318 let mut engine = FormatEngine::new(descs);
1319 let out = engine.format_values(&[IoValue::Real(1.234)]);
1320 // RU rounds 1.234 up to 1.24 at 2 decimals.
1321 assert_eq!(out.trim(), "1.24");
1322 }
1323
1324 #[test]
1325 fn format_rounding_down() {
1326 let descs = parse_format("(RD, F6.2)");
1327 let mut engine = FormatEngine::new(descs);
1328 let out = engine.format_values(&[IoValue::Real(1.236)]);
1329 // RD rounds 1.236 down to 1.23 at 2 decimals.
1330 assert_eq!(out.trim(), "1.23");
1331 }
1332
1333 #[test]
1334 fn format_rounding_zero() {
1335 let descs = parse_format("(RZ, F6.2)");
1336 let mut engine = FormatEngine::new(descs);
1337 let out = engine.format_values(&[IoValue::Real(-1.236)]);
1338 // RZ truncates toward zero: -1.236 → -1.23.
1339 assert_eq!(out.trim(), "-1.23");
1340 }
1341
1342 #[test]
1343 fn format_scale_factor_f() {
1344 // 2P with F: multiplies value by 10^2 before formatting.
1345 let descs = parse_format("(2P, F8.2)");
1346 let mut engine = FormatEngine::new(descs);
1347 let out = engine.format_values(&[IoValue::Real(1.5)]);
1348 // 1.5 * 100 = 150.00
1349 assert_eq!(out.trim(), "150.00");
1350 }
1351
1352 #[test]
1353 fn format_d_descriptor() {
1354 let descs = parse_format("(D12.5)");
1355 assert!(matches!(
1356 descs[0],
1357 FormatDesc::RealD {
1358 width: 12,
1359 decimals: 5
1360 }
1361 ));
1362 }
1363
1364 #[test]
1365 fn format_d_vs_dc() {
1366 // D12.5 is a real descriptor; DC is decimal comma mode.
1367 let descs = parse_format("(DC, D12.5)");
1368 assert!(matches!(
1369 descs[0],
1370 FormatDesc::DecimalMode(DecimalSep::Comma)
1371 ));
1372 assert!(matches!(
1373 descs[1],
1374 FormatDesc::RealD {
1375 width: 12,
1376 decimals: 5
1377 }
1378 ));
1379 }
1380
1381 #[test]
1382 fn format_integer16_full_width() {
1383 let descs = parse_format("(I40)");
1384 let mut engine = FormatEngine::new(descs);
1385 let out = engine.format_values(&[IoValue::Integer(
1386 170141183460469231731687303715884105727i128,
1387 )]);
1388 assert_eq!(out, " 170141183460469231731687303715884105727");
1389 }
1390 }
1391