Skip to main content

ironwork_rt/le/
mod.rs

1//! Language Environment callable services (SA38-0683-60): which ones ironwork provides, their
2//! condition tokens, Lilian dates and seconds, and the picture strings of the date and time
3//! services. `services` reads and writes a CALL's arguments and runs each service. What the manual
4//! leaves open is `numeric::assumptions` L1 to L18.
5
6mod services;
7
8pub use services::{LeHost, call};
9
10use crate::calendar::{civil, days_in_month, is_leap, lilian, weekday, LAST_LILIAN, LILIAN_ZERO, MILLIS_PER_DAY, SECONDS_PER_DAY};
11use crate::feedback;
12use crate::lir::LeService;
13use numeric::precision::{Fixed, Places};
14use zarch::check::ProgramMask;
15use zarch::ebcdic::{self, CodePage};
16use zarch::hfp::{Hfp, Precision, Rounding};
17use zarch::wide::U256;
18
19/// What a run keeps for the services: heap storage, and which output DDs it has started.
20#[derive(Debug, Default)]
21pub struct State {
22    /// Each CEEGTST block: where it starts, its length, and whether CEEFRST has freed it.
23    pub heap: Vec<(usize, usize, bool)>,
24    pub written: Vec<String>,
25}
26
27impl State {
28    /// The end of the highest heap block: run-unit storage below it is not released.
29    pub fn heap_end(&self) -> usize {
30        self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
31    }
32}
33
34/// The services ironwork provides, each with its parameters in order; `fc` is always last.
35pub const PROVIDED: &[(LeService, &str, &[&str])] = &[
36    (LeService::Cee3abd, "CEE3ABD", &["abcode", "clean-up"]),
37    (LeService::Cee3dmp, "CEE3DMP", &["title", "options", "fc"]),
38    (LeService::Ceedate, "CEEDATE", &["input_Lilian_date", "picture_string", "output_char_date", "fc"]),
39    (LeService::Ceedatm, "CEEDATM", &["input_seconds", "picture_string", "output_timestamp", "fc"]),
40    (LeService::Ceedays, "CEEDAYS", &["input_char_date", "picture_string", "output_Lilian_date", "fc"]),
41    (LeService::Ceedywk, "CEEDYWK", &["input_Lilian_date", "output_day_no", "fc"]),
42    (LeService::Ceefrst, "CEEFRST", &["address", "fc"]),
43    (LeService::Ceegmt, "CEEGMT", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
44    (LeService::Ceegmto, "CEEGMTO", &["offset_hours", "offset_minutes", "offset_seconds", "fc"]),
45    (LeService::Ceegtst, "CEEGTST", &["heap_id", "size", "address", "fc"]),
46    (LeService::Ceeloct, "CEELOCT", &["output_Lilian", "output_seconds", "output_Gregorian", "fc"]),
47    (LeService::Ceemout, "CEEMOUT", &["message_string", "destination_code", "fc"]),
48    (LeService::Ceesecs, "CEESECS", &["input_timestamp", "picture_string", "output_seconds", "fc"]),
49    (LeService::Ceeutc, "CEEUTC", &["output_GMT_Lilian", "output_GMT_seconds", "fc"]),
50];
51
52/// Every callable service SA38-0683-60 documents, other than the math services.
53const DOCUMENTED: &[&str] = &[
54    "CEE3AB2", "CEE3ABD", "CEE3CIB", "CEE3CTY", "CEE3DLY", "CEE3DMP", "CEE3GRC", "CEE3GRN", "CEE3GRO", "CEE3INF", "CEE3LNG", "CEE3MC2", "CEE3MCS",
55    "CEE3MDS", "CEE3MTS", "CEE3PR2", "CEE3PRM", "CEE3RPH", "CEE3SPM", "CEE3SRC", "CEE3SRP", "CEE3USR", "CEECBLDY", "CEECMI", "CEECRHP", "CEECZST",
56    "CEEDATE", "CEEDATM", "CEEDAYS", "CEEDCOD", "CEEDLYM", "CEEDSHP", "CEEDYWK", "CEEENV", "CEEFMDA", "CEEFMDT", "CEEFMON", "CEEFMTM", "CEEFRST",
57    "CEEFTDS", "CEEGMT", "CEEGMTO", "CEEGPID", "CEEGQDT", "CEEGTJS", "CEEGTST", "CEEHDLR", "CEEHDLU", "CEEISEC", "CEEITOK", "CEELCNV", "CEELOCT",
58    "CEEMGET", "CEEMICT", "CEEMOUT", "CEEMRCE", "CEEMRCR", "CEEMSG", "CEENCOD", "CEEQCEN", "CEEQDTC", "CEEQRYL", "CEERAN0", "CEERCDM", "CEESCEN",
59    "CEESCOL", "CEESECI", "CEESECS", "CEESETL", "CEESGL", "CEESICLR", "CEESISET", "CEESISHF", "CEESITST", "CEESTXF", "CEETDLI", "CEETEST", "CEEUSGD",
60    "CEEUTC",
61];
62
63/// The math services are CEESxnnn, x naming the operand type.
64const MATH: &[&str] = &[
65    "ABS", "ACS", "ASN", "ATH", "ATN", "AT2", "CJG", "COS", "CSH", "CTN", "DIM", "DVD", "ERC", "ERF", "EXP", "GMA", "IMG", "INT", "LGM", "LG1", "LG2",
66    "LOG", "MLT", "MOD", "NIN", "NWN", "SGN", "SIN", "SNH", "SQT", "TAN", "TNH",
67];
68
69pub fn parameters(name: &str) -> Option<&'static [&'static str]> {
70    PROVIDED.iter().find(|(_, n, _)| *n == name).map(|(_, _, p)| *p)
71}
72
73pub fn provides(name: &str) -> bool {
74    parameters(name).is_some()
75}
76
77/// The service a CALL of this name runs when no program has it.
78pub fn service(name: &str) -> Option<LeService> {
79    PROVIDED.iter().find(|(_, n, _)| *n == name).map(|(s, _, _)| *s)
80}
81
82pub fn name(service: LeService) -> &'static str {
83    PROVIDED.iter().find(|(s, _, _)| *s == service).map_or("", |(_, n, _)| *n)
84}
85
86fn documented(name: &str) -> bool {
87    let math = name.len() == 8 && name.starts_with("CEES") && (MATH.contains(&&name[5..]) || name[5..].starts_with("XP"));
88    DOCUMENTED.contains(&name) || math
89}
90
91/// The S806 message for a CALL that finds nothing.
92pub fn missing(name: &str) -> String {
93    if documented(name) {
94        format!("CALL {name}: {name} is a Language Environment callable service that ironwork for COBOL does not provide yet")
95    } else {
96        format!("CALL {name}: no such program in the run unit or its program libraries")
97    }
98}
99
100/// A condition of facility CEE: its severity and message number.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct Condition {
103    pub severity: u8,
104    pub number: u16,
105}
106
107const fn condition(severity: u8, number: u16) -> Condition {
108    Condition { severity, number }
109}
110
111pub const DESTINATION: Condition = condition(3, 451);
112pub const HEAP_ID: Condition = condition(3, 803);
113pub const HEAP_SIZE: Condition = condition(3, 808);
114pub const FREE_ADDRESS: Condition = condition(3, 810);
115pub const HEAP_SHORT: Condition = condition(3, 813);
116pub const SECONDS_RANGE: Condition = condition(3, 2505);
117pub const INSUFFICIENT: Condition = condition(3, 2507);
118pub const DATE_VALUE: Condition = condition(3, 2508);
119pub const HOURS: Condition = condition(3, 2510);
120pub const LILIAN_RANGE: Condition = condition(3, 2512);
121pub const DATE_RANGE: Condition = condition(3, 2513);
122pub const MINUTES: Condition = condition(3, 2516);
123pub const MONTH: Condition = condition(3, 2517);
124pub const PICTURE: Condition = condition(3, 2518);
125pub const SECONDS_VALUE: Condition = condition(3, 2519);
126pub const DAYS_NONNUMERIC: Condition = condition(3, 2520);
127pub const SECS_NONNUMERIC: Condition = condition(3, 2525);
128pub const DATE_TRUNCATED: Condition = condition(2, 2526);
129pub const TIMESTAMP_TRUNCATED: Condition = condition(2, 2527);
130pub const DUMP_OPTIONS: Condition = condition(2, 3102);
131
132impl Condition {
133    pub fn symbol(self) -> String {
134        feedback::symbol(self.number)
135    }
136
137    /// The 12-byte condition token, with no instance-specific information.
138    pub fn token(self) -> [u8; 12] {
139        let mut t = [0u8; 12];
140        t[..8].copy_from_slice(&feedback::token(self.severity, self.number));
141        t
142    }
143
144    pub fn text(self) -> &'static str {
145        match self.number {
146            451 => "CEEMOUT was given a destination code it does not accept.",
147            803 => "No heap has the identifier given to a get-storage or discard-heap request.",
148            808 => "A get-storage (CEEGTST) or reallocate (CEECZST) request asked for a size that is not positive.",
149            810 => "CEEFRST was given an address that is not heap storage, or the heap's control information is damaged.",
150            813 => "There was not enough storage to satisfy a get-storage request.",
151            2505 => "The number of seconds is outside the range the service supports.",
152            2507 => "CEEDAYS or CEESECS ran out of input before the date was complete, so no Lilian value was produced.",
153            2508 => "CEEDAYS or CEESECS was given a date that does not exist.",
154            2510 => "CEEISEC or CEESECS was given an hour it cannot accept.",
155            2512 => "CEEDATE or CEEDYWK was given a Lilian day outside the supported range.",
156            2513 => "CEEISEC, CEEDAYS or CEESECS was given a date outside the supported range.",
157            2516 => "CEEISEC was given a minute it cannot accept.",
158            2517 => "CEEISEC was given a month it cannot accept.",
159            2518 => "A date or time service was given a picture string it cannot use.",
160            2519 => "CEEISEC was given a second it cannot accept.",
161            2520 => "CEEDAYS found a non-digit where its picture wants a number, or the date does not fit the picture.",
162            2525 => "CEESECS found a non-digit where its picture wants a number, or the timestamp does not fit the picture.",
163            2526 => "The date CEEDATE produced did not fit its output field and was cut short.",
164            2527 => "The timestamp CEEDATM produced did not fit its output field and was cut short.",
165            3102 => "CEE3DMP ignored options or suboptions it does not recognise.",
166            _ => "",
167        }
168    }
169}
170
171/// An instant as a Lilian day and the milliseconds into it.
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct Stamp {
174    pub lilian: i64,
175    pub millis: i64,
176}
177
178impl Stamp {
179    /// The clock's reading: Unix seconds and hundredths.
180    pub fn from_unix(seconds: i64, hundredths: u32) -> Self {
181        Self { lilian: seconds.div_euclid(SECONDS_PER_DAY) - LILIAN_ZERO, millis: seconds.rem_euclid(SECONDS_PER_DAY) * 1000 + i64::from(hundredths.min(99)) * 10 }
182    }
183
184    pub fn from_millis(ms: i64) -> Self {
185        Self { lilian: ms.div_euclid(MILLIS_PER_DAY), millis: ms.rem_euclid(MILLIS_PER_DAY) }
186    }
187
188    pub fn total_millis(self) -> i64 {
189        self.lilian.saturating_mul(MILLIS_PER_DAY).saturating_add(self.millis)
190    }
191
192    pub fn fields(self) -> Fields {
193        let c = civil(self.lilian.saturating_add(LILIAN_ZERO).saturating_mul(SECONDS_PER_DAY));
194        let s = self.millis / 1000;
195        Fields {
196            year: c.year,
197            month: c.month,
198            day: c.day,
199            yday: c.day_of_year,
200            weekday: weekday(self.lilian),
201            hour: (s / 3600) as u32,
202            minute: (s / 60 % 60) as u32,
203            second: (s % 60) as u32,
204            millis: (self.millis % 1000) as u32,
205        }
206    }
207
208    /// YYYYMMDDHHMISS999, as CEELOCT gives it.
209    pub fn gregorian(self) -> String {
210        let f = self.fields();
211        format!("{:04}{:02}{:02}{:02}{:02}{:02}{:03}", f.year, f.month, f.day, f.hour, f.minute, f.second, f.millis)
212    }
213}
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub struct Fields {
217    pub year: i64,
218    pub month: u32,
219    pub day: u32,
220    pub yday: u32,
221    pub weekday: u32,
222    pub hour: u32,
223    pub minute: u32,
224    pub second: u32,
225    pub millis: u32,
226}
227
228/// Lilian seconds as a COMP-2: the millisecond count, converted and divided by 1000 in long HFP.
229pub fn seconds_hfp(total_millis: i64) -> [u8; 8] {
230    let value = Fixed { negative: total_millis < 0, magnitude: U256::from_u128(u128::from(total_millis.unsigned_abs())), places: Places::new(16, 3) };
231    let bytes = numeric::float::from_fixed(value, Precision::Long, ProgramMask::default()).map_or(vec![0; 8], Hfp::to_bytes);
232    bytes.try_into().unwrap_or([0; 8])
233}
234
235/// A COMP-2 of Lilian seconds, to the nearest millisecond; None when it is not a number of
236/// milliseconds a fullword-pair can hold.
237pub fn hfp_millis(bytes: [u8; 8]) -> Option<i64> {
238    let (negative, ms) = Hfp::from_bytes(Precision::Long, &bytes).to_scaled_integer(3, Rounding::HalfAwayFromZero)?;
239    let ms = i64::try_from(ms.to_u128()?).ok()?;
240    Some(if negative { -ms } else { ms })
241}
242
243const MONTHS: [&str; 12] = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"];
244const DAYS: [&str; 7] = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"];
245const ROMAN: [&str; 12] = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
246
247/// One term of a date and time picture string (SA38-0683-60, Table 34).
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub enum Term {
250    /// Y, YY or YYYY.
251    Year(u8),
252    /// MM, or ZM with its leading zero suppressed.
253    Month(bool),
254    /// MMM, Mmm and longer: each letter's case is the output's; Z suppresses trailing blanks.
255    MonthName { upper: Vec<bool>, trim: bool },
256    /// RRRR, or RRRZ.
257    Roman(bool),
258    Day(bool),
259    DayOfYear,
260    Hour(bool),
261    Minute,
262    Second,
263    /// 9, 99 or 999.
264    Fraction(u8),
265    Meridiem { lower: bool, dots: bool },
266    Weekday,
267    WeekdayName { upper: Vec<bool>, trim: bool },
268    /// <JJJJ>, <CCCC>, <CCCCCCCC>, YYY and ZYY, which ironwork does not provide.
269    Era,
270    /// A delimiter, kept as its byte.
271    Literal(u8),
272}
273
274impl Term {
275    fn is_time(&self) -> bool {
276        matches!(self, Term::Hour(_) | Term::Minute | Term::Second | Term::Fraction(_) | Term::Meridiem { .. })
277    }
278}
279
280/// The US defaults for a null or blank picture string (SA38-0683-60, Table 33).
281const DEFAULT_DATE: &str = "MM/DD/YY";
282const DEFAULT_TIMESTAMP: &str = "MM/DD/YY ZH:MI:SS AP";
283
284fn blank(picture: &[u8]) -> bool {
285    picture.iter().all(|&b| b == ebcdic::SPACE)
286}
287
288fn picture_or_default(picture: &[u8], default: &str, page: &CodePage) -> Vec<u8> {
289    if blank(picture) { page.encode(default).unwrap_or_default() } else { picture.to_vec() }
290}
291
292pub fn terms(picture: &[u8], page: &CodePage) -> Vec<Term> {
293    let chars: Vec<char> = picture.iter().map(|&b| page.decode_byte(b)).collect();
294    let at = |i: usize| chars.get(i).copied().unwrap_or('\0');
295    let run = |i: usize, f: fn(char) -> bool| chars[i..].iter().take_while(|&&c| f(c)).count();
296    let starts = |i: usize, s: &str| chars[i..].iter().copied().take(s.chars().count()).eq(s.chars());
297    // A Z or z after a name suppresses its blanks, unless it starts ZM, ZD, ZH or ZYY.
298    let trim = |i: usize| matches!(at(i), 'Z' | 'z') && !(at(i) == 'Z' && (matches!(at(i + 1), 'M' | 'D' | 'H') || starts(i + 1, "YY")));
299    let mut out: Vec<Term> = Vec::new();
300    let mut i = 0;
301    while i < chars.len() {
302        let after_hour = matches!(out.iter().rev().find(|t| !matches!(t, Term::Literal(_))), Some(Term::Hour(_)));
303        let (term, used) = match chars[i] {
304            'Y' => match run(i, |c| c == 'Y').min(4) {
305                3 => (Term::Era, 3),
306                n => (Term::Year(n as u8), n),
307            },
308            'Z' => match at(i + 1) {
309                'M' => (Term::Month(true), 2),
310                'D' => (Term::Day(true), 2),
311                'H' => (Term::Hour(true), 2),
312                'Y' if at(i + 2) == 'Y' => (Term::Era, 3),
313                _ => (Term::Literal(picture[i]), 1),
314            },
315            'M' if at(i + 1) == 'I' => (Term::Minute, 2),
316            'M' => match run(i, |c| c == 'M' || c == 'm') {
317                1 => (Term::Literal(picture[i]), 1),
318                2 if at(i + 1) == 'M' => (if after_hour { Term::Minute } else { Term::Month(false) }, 2),
319                2 => (Term::Literal(picture[i]), 1),
320                n => {
321                    let width = n.min(20);
322                    let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
323                    let trim = width == n && trim(i + width);
324                    (Term::MonthName { upper, trim }, width + usize::from(trim))
325                }
326            },
327            'W' => match run(i, |c| c == 'W' || c == 'w') {
328                1 | 2 => (Term::Weekday, 1),
329                n => {
330                    let width = n.min(20);
331                    let upper = chars[i..i + width].iter().map(|c| c.is_ascii_uppercase()).collect();
332                    let trim = width == n && trim(i + width);
333                    (Term::WeekdayName { upper, trim }, width + usize::from(trim))
334                }
335            },
336            'R' => match run(i, |c| c == 'R') {
337                n if n >= 4 => (Term::Roman(false), 4),
338                3 if at(i + 3) == 'Z' => (Term::Roman(true), 4),
339                _ => (Term::Literal(picture[i]), 1),
340            },
341            'D' => match run(i, |c| c == 'D') {
342                1 => (Term::Literal(picture[i]), 1),
343                2 => (Term::Day(false), 2),
344                _ => (Term::DayOfYear, 3),
345            },
346            'H' if at(i + 1) == 'H' => (Term::Hour(false), 2),
347            'S' if at(i + 1) == 'S' => (Term::Second, 2),
348            '9' => {
349                let n = run(i, |c| c == '9').min(3);
350                (Term::Fraction(n as u8), n)
351            }
352            'A' | 'a' if starts(i + 1, if chars[i] == 'A' { "P" } else { "p" }) => (Term::Meridiem { lower: chars[i] == 'a', dots: false }, 2),
353            'A' | 'a' if starts(i, if chars[i] == 'A' { "A.P." } else { "a.p." }) => (Term::Meridiem { lower: chars[i] == 'a', dots: true }, 4),
354            '<' if starts(i, "<JJJJ>") || starts(i, "<CCCC>") => (Term::Era, 6),
355            '<' if starts(i, "<CCCCCCCC>") => (Term::Era, 10),
356            _ => (Term::Literal(picture[i]), 1),
357        };
358        out.push(term);
359        i += used;
360    }
361    out
362}
363
364/// A name in the case its picture term asks for, padded or cut to the term's width.
365fn cased(name: &str, upper: &[bool], trim: bool) -> String {
366    let mut s: String = name.chars().zip(upper).map(|(c, &u)| if u { c } else { c.to_ascii_lowercase() }).collect();
367    if !trim {
368        while s.len() < upper.len() {
369            s.push(' ');
370        }
371    }
372    s
373}
374
375/// Writes a date, or a timestamp when `time` (CEEDATM); without it every time term is zero, as
376/// CEEDATE shows them (SA38-0683-60, Table 27).
377pub fn format(terms: &[Term], f: &Fields, time: bool, page: &CodePage) -> Vec<u8> {
378    let twelve = time && terms.iter().any(|t| matches!(t, Term::Meridiem { .. }));
379    let (hour, minute, second, millis) = if time { (f.hour, f.minute, f.second, f.millis) } else { (0, 0, 0, 0) };
380    let clock_hour = if twelve && hour % 12 == 0 { 12 } else if twelve { hour % 12 } else { hour };
381    let mut out = Vec::new();
382    for t in terms {
383        let text = match t {
384            Term::Literal(b) => {
385                out.push(*b);
386                continue;
387            }
388            Term::Year(4) => format!("{:04}", f.year),
389            Term::Year(2) => format!("{:02}", f.year % 100),
390            Term::Year(_) => format!("{}", f.year % 10),
391            Term::Month(true) => f.month.to_string(),
392            Term::Month(false) => format!("{:02}", f.month),
393            Term::MonthName { upper, trim } => cased(MONTHS[f.month as usize - 1], upper, *trim),
394            Term::Roman(trim) => cased(ROMAN[f.month as usize - 1], &[true; 4], *trim),
395            Term::Day(true) => f.day.to_string(),
396            Term::Day(false) => format!("{:02}", f.day),
397            Term::DayOfYear => format!("{:03}", f.yday),
398            Term::Hour(true) => clock_hour.to_string(),
399            Term::Hour(false) => format!("{clock_hour:02}"),
400            Term::Minute => format!("{minute:02}"),
401            Term::Second => format!("{second:02}"),
402            Term::Fraction(n) => format!("{:03}", millis)[..*n as usize].to_owned(),
403            Term::Meridiem { lower, dots } => {
404                let pm = time && hour >= 12;
405                let s = match (pm, dots) {
406                    (false, false) => "AM",
407                    (true, false) => "PM",
408                    (false, true) => "A.M.",
409                    (true, true) => "P.M.",
410                };
411                if *lower { s.to_ascii_lowercase() } else { s.to_owned() }
412            }
413            Term::Weekday => DAYS[f.weekday as usize - 1][..1].to_owned(),
414            Term::WeekdayName { upper, trim } => cased(DAYS[f.weekday as usize - 1], upper, *trim),
415            Term::Era => String::new(),
416        };
417        out.extend(text.chars().map(|c| page.encode_char(c).unwrap_or(ebcdic::SPACE)));
418    }
419    out
420}
421
422/// A fixed-length 80-character output, and whether the formatted text was longer.
423fn output_80(mut text: Vec<u8>) -> (Vec<u8>, bool) {
424    let truncated = text.len() > 80;
425    text.resize(80, ebcdic::SPACE);
426    (text, truncated)
427}
428
429/// CEEDATE: a Lilian date in the picture's form, 80 bytes, and the condition if any.
430pub fn date(lilian: i64, picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
431    let blanks = vec![ebcdic::SPACE; 80];
432    if !(1..=LAST_LILIAN).contains(&lilian) {
433        return (blanks, Some(LILIAN_RANGE));
434    }
435    let terms = terms(&picture_or_default(picture, DEFAULT_DATE, page), page);
436    if terms.contains(&Term::Era) {
437        return (blanks, Some(PICTURE));
438    }
439    let fields = Stamp { lilian, millis: 0 }.fields();
440    let (text, truncated) = output_80(format(&terms, &fields, false, page));
441    (text, truncated.then_some(DATE_TRUNCATED))
442}
443
444/// CEEDATM: Lilian seconds in the picture's form, 80 bytes, and the condition if any.
445pub fn timestamp(seconds: [u8; 8], picture: &[u8], page: &CodePage) -> (Vec<u8>, Option<Condition>) {
446    let blanks = vec![ebcdic::SPACE; 80];
447    let Some(ms) = hfp_millis(seconds).filter(|ms| (MILLIS_PER_DAY..(LAST_LILIAN + 1) * MILLIS_PER_DAY).contains(ms)) else {
448        return (blanks, Some(SECONDS_RANGE));
449    };
450    let terms = terms(&picture_or_default(picture, DEFAULT_TIMESTAMP, page), page);
451    if terms.contains(&Term::Era) {
452        return (blanks, Some(PICTURE));
453    }
454    let (text, truncated) = output_80(format(&terms, &Stamp::from_millis(ms).fields(), true, page));
455    (text, truncated.then_some(TIMESTAMP_TRUNCATED))
456}
457
458/// Which service is reading a date: CEEDAYS ignores time terms; CEESECS reads them.
459#[derive(Clone, Copy, Debug, PartialEq, Eq)]
460pub enum Reading {
461    Days,
462    Seconds,
463}
464
465/// A numeric field of up to `width` digits, leading blanks allowed. A field before a delimiter may
466/// be shorter, as 6/2/88 is for MM/DD/YY; any other must be full. Width 0 is a delimiter, which
467/// takes one character, whatever it is.
468fn number(input: &[char], at: &mut usize, width: usize, delimited: bool, nonnumeric: Condition) -> Result<u32, Condition> {
469    if width == 0 {
470        *at += 1;
471        return Ok(0);
472    }
473    let (mut value, mut digits, mut taken) = (0u32, 0, 0);
474    while taken < width && *at + taken < input.len() {
475        match input[*at + taken] {
476            ' ' if digits == 0 => {}
477            c if c.is_ascii_digit() => {
478                value = value * 10 + c.to_digit(10).unwrap_or(0);
479                digits += 1;
480            }
481            _ => break,
482        }
483        taken += 1;
484    }
485    *at += taken;
486    let complete = digits > 0 && (taken == width || delimited);
487    match (complete, *at < input.len()) {
488        (true, _) => Ok(value),
489        (false, true) => Err(nonnumeric),
490        (false, false) => Err(INSUFFICIENT),
491    }
492}
493
494/// The run of characters `f` accepts, at most 20, uppercased.
495fn word(input: &[char], at: &mut usize, f: fn(&char) -> bool) -> String {
496    let w: String = input[*at..].iter().take_while(|c| f(c)).take(20).collect();
497    *at += w.chars().count();
498    w.to_ascii_uppercase()
499}
500
501#[derive(Default)]
502struct Parsed {
503    year: Option<(i64, bool)>,
504    month: Option<u32>,
505    day: Option<u32>,
506    yday: Option<u32>,
507    hour: Option<u32>,
508    minute: u32,
509    second: u32,
510    millis: u32,
511    pm: Option<bool>,
512}
513
514/// CEEDAYS and CEESECS: a date or timestamp read by its picture. `window` is the first year of
515/// the hundred a two-digit year falls in.
516pub fn read(input: &[u8], picture: &[u8], reading: Reading, window: i64, page: &CodePage) -> Result<Stamp, Condition> {
517    let default = if reading == Reading::Days { DEFAULT_DATE } else { DEFAULT_TIMESTAMP };
518    let picture = picture_or_default(picture, default, page);
519    let terms = terms(&picture, page);
520    if terms.iter().any(|t| matches!(t, Term::Era | Term::Year(1))) {
521        return Err(PICTURE);
522    }
523    let nonnumeric = if reading == Reading::Days { DAYS_NONNUMERIC } else { SECS_NONNUMERIC };
524    let input: Vec<char> = input.iter().map(|&b| page.decode_byte(b)).collect();
525    let lead = terms.iter().take_while(|t| **t == Term::Literal(ebcdic::SPACE)).count();
526    let mut at = if lead > 0 { lead } else { input.iter().take_while(|&&c| c == ' ').count() };
527    let mut p = Parsed::default();
528    for (k, t) in terms.iter().enumerate().skip(lead) {
529        if at >= input.len() {
530            if terms[k..].iter().any(|t| !t.is_time() && !matches!(t, Term::Literal(_) | Term::Weekday | Term::WeekdayName { .. })) {
531                return Err(INSUFFICIENT);
532            }
533            break;
534        }
535        let delimited = matches!(terms.get(k + 1), Some(Term::Literal(_)));
536        let mut number = |width: u8| number(&input, &mut at, width as usize, delimited, nonnumeric);
537        let outcome = match t {
538            Term::Literal(_) => number(0).map(drop),
539            Term::Year(n) => number(*n).map(|y| p.year = Some((i64::from(y), *n == 2))),
540            Term::Month(_) => number(2).map(|m| p.month = Some(m)),
541            Term::Day(_) => number(2).map(|d| p.day = Some(d)),
542            Term::DayOfYear => number(3).map(|d| p.yday = Some(d)),
543            Term::Hour(_) => number(2).map(|h| p.hour = Some(h)),
544            Term::Minute => number(2).map(|m| p.minute = m),
545            Term::Second => number(2).map(|s| p.second = s),
546            Term::Fraction(n) => number(*n).map(|f| p.millis = f * 10u32.pow(3 - u32::from(*n))),
547            Term::MonthName { .. } => {
548                let word = word(&input, &mut at, |c| c.is_ascii_alphabetic());
549                MONTHS.iter().position(|m| word.len() >= 3 && m.starts_with(&word)).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
550            }
551            Term::Roman(_) => {
552                let word = word(&input, &mut at, |c| matches!(c, 'I' | 'V' | 'X' | 'i' | 'v' | 'x'));
553                ROMAN.iter().position(|r| *r == word).map(|m| p.month = Some(m as u32 + 1)).ok_or(MONTH)
554            }
555            Term::Weekday | Term::WeekdayName { .. } => {
556                word(&input, &mut at, |c| c.is_ascii_alphabetic());
557                Ok(())
558            }
559            Term::Meridiem { dots, .. } => {
560                let word: String = input[at..].iter().take(if *dots { 4 } else { 2 }).collect::<String>().to_ascii_uppercase();
561                at += word.chars().count();
562                match word.as_str() {
563                    "AM" | "A.M." => Some(false),
564                    "PM" | "P.M." => Some(true),
565                    _ => None,
566                }
567                .map(|pm| p.pm = Some(pm))
568                .ok_or(nonnumeric)
569            }
570            Term::Era => Err(PICTURE),
571        };
572        if !(reading == Reading::Days && t.is_time()) {
573            outcome?;
574        }
575    }
576    let Some((year, two_digit)) = p.year else { return Err(INSUFFICIENT) };
577    let year = if two_digit { window + (year - window).rem_euclid(100) } else { year };
578    let lilian = match (p.month, p.day, p.yday) {
579        (_, _, Some(yday)) if p.month.is_none() || p.day.is_none() => {
580            let length = if is_leap(year) { 366 } else { 365 };
581            if !(1..=length).contains(&yday) {
582                return Err(DATE_VALUE);
583            }
584            lilian(year, 1, 1) + i64::from(yday) - 1
585        }
586        (Some(month), Some(day), _) => {
587            if !(1..=12).contains(&month) {
588                return Err(MONTH);
589            }
590            if !(1..=days_in_month(year, month)).contains(&day) {
591                return Err(DATE_VALUE);
592            }
593            lilian(year, month, day)
594        }
595        _ => return Err(INSUFFICIENT),
596    };
597    if !(1582..=9999).contains(&year) || !(1..=LAST_LILIAN).contains(&lilian) {
598        return Err(DATE_RANGE);
599    }
600    if reading == Reading::Days {
601        return Ok(Stamp { lilian, millis: 0 });
602    }
603    let hour = match (p.hour.unwrap_or(0), p.pm) {
604        (h, None) if h <= 23 => h,
605        (h @ 1..=12, Some(pm)) => h % 12 + if pm { 12 } else { 0 },
606        _ => return Err(HOURS),
607    };
608    if p.minute > 59 {
609        return Err(MINUTES);
610    }
611    if p.second > 59 {
612        return Err(SECONDS_VALUE);
613    }
614    let millis = ((i64::from(hour) * 60 + i64::from(p.minute)) * 60 + i64::from(p.second)) * 1000 + i64::from(p.millis);
615    Ok(Stamp { lilian, millis })
616}
617
618/// CEE3DMP's options: the ddname FNAME names, and whether any keyword was not one it takes.
619pub fn dump_options(options: &str) -> (String, bool) {
620    const KEYWORDS: &[(&str, usize)] = &[
621        ("ENCLAVE", 4), ("THREAD", 3), ("TRACEBACK", 5), ("NOTRACEBACK", 7), ("FILES", 4), ("NOFILES", 6), ("VARIABLES", 3),
622        ("NOVARIABLES", 5), ("BLOCKS", 5), ("NOBLOCKS", 7), ("STORAGE", 4), ("NOSTORAGE", 6), ("REGSTOR", 4), ("STACKFRAME", 2),
623        ("PAGESIZE", 4), ("FNAME", 5), ("CONDITION", 4), ("NOCONDITION", 6), ("ENTRY", 5), ("NOENTRY", 7), ("GENOPTS", 4), ("NOGENOPTS", 6),
624    ];
625    let mut dd = "CEEDUMP".to_owned();
626    let mut invalid = false;
627    let upper = options.to_ascii_uppercase();
628    let mut rest = upper.as_str();
629    loop {
630        rest = rest.trim_start_matches([' ', ',']);
631        if rest.is_empty() {
632            break;
633        }
634        let word_end = rest.find([' ', ',', '(']).unwrap_or(rest.len());
635        let (word, after) = rest.split_at(word_end);
636        let (argument, after) = match after.strip_prefix('(') {
637            Some(inner) => match inner.split_once(')') {
638                Some((a, tail)) => (Some(a.trim()), tail),
639                None => {
640                    invalid = true;
641                    (Some(inner.trim()), "")
642                }
643            },
644            None => (None, after),
645        };
646        match KEYWORDS.iter().find(|(k, min)| word.len() >= *min && k.starts_with(word)) {
647            Some(("FNAME", _)) => match argument.filter(|a| !a.is_empty()) {
648                Some(a) => dd = a.to_owned(),
649                None => invalid = true,
650            },
651            Some(_) => {}
652            None => invalid = true,
653        }
654        rest = after;
655    }
656    (dd, invalid)
657}
658
659#[cfg(test)]
660mod tests;