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