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