1mod 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#[derive(Debug, Default)]
21pub struct State {
22 pub heap: Vec<(usize, usize, bool)>,
24 pub written: Vec<String>,
25}
26
27impl State {
28 pub fn heap_end(&self) -> usize {
30 self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
31 }
32}
33
34pub 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
52const 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
63const 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
77pub 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
91pub 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#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct Stamp {
174 pub lilian: i64,
175 pub millis: i64,
176}
177
178impl Stamp {
179 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 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
228pub 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
235pub 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#[derive(Clone, Debug, PartialEq, Eq)]
249pub enum Term {
250 Year(u8),
252 Month(bool),
254 MonthName { upper: Vec<bool>, trim: bool },
256 Roman(bool),
258 Day(bool),
259 DayOfYear,
260 Hour(bool),
261 Minute,
262 Second,
263 Fraction(u8),
265 Meridiem { lower: bool, dots: bool },
266 Weekday,
267 WeekdayName { upper: Vec<bool>, trim: bool },
268 Era,
270 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
280const 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 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
364fn 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
375pub 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
422fn 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
429pub 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
444pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
460pub enum Reading {
461 Days,
462 Seconds,
463}
464
465fn 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
494fn 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
514pub 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
618pub 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;