1pub 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#[derive(Debug, Default)]
22pub struct State {
23 pub heap: Vec<(usize, usize, bool)>,
25 pub written: Vec<String>,
26}
27
28impl State {
29 pub fn heap_end(&self) -> usize {
31 self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
32 }
33}
34
35pub 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
53const 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
64const 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
78pub 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
92pub 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#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub struct Stamp {
175 pub lilian: i64,
176 pub millis: i64,
177}
178
179impl Stamp {
180 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 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
229pub 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
236pub 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#[derive(Clone, Debug, PartialEq, Eq)]
250pub enum Term {
251 Year(u8),
253 Month(bool),
255 MonthName { upper: Vec<bool>, trim: bool },
257 Roman(bool),
259 Day(bool),
260 DayOfYear,
261 Hour(bool),
262 Minute,
263 Second,
264 Fraction(u8),
266 Meridiem { lower: bool, dots: bool },
267 Weekday,
268 WeekdayName { upper: Vec<bool>, trim: bool },
269 Era,
271 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
281const 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 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
365fn 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
376pub 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
423fn 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
430pub 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
445pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum Reading {
462 Days,
463 Seconds,
464}
465
466fn 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
495fn 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
515pub 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
619pub 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;