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
20pub const HEAP_LIMIT: usize = 1 << 28;
22
23#[derive(Debug, Default)]
25pub struct State {
26 pub heap: Vec<(usize, usize, bool)>,
28 pub written: Vec<String>,
29}
30
31impl State {
32 pub fn heap_end(&self) -> usize {
34 self.heap.iter().map(|&(at, len, _)| at + len).max().unwrap_or(0)
35 }
36}
37
38pub 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
56const 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
67const 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
81pub 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
95pub 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
111pub const MODULE_NOT_FOUND: &str = "CEE3501S";
113
114pub 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#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub struct Stamp {
193 pub lilian: i64,
194 pub millis: i64,
195}
196
197impl Stamp {
198 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 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
247pub 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
254pub 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#[derive(Clone, Debug, PartialEq, Eq)]
268pub enum Term {
269 Year(u8),
271 Month(bool),
273 MonthName { upper: Vec<bool>, trim: bool },
275 Roman(bool),
277 Day(bool),
278 DayOfYear,
279 Hour(bool),
280 Minute,
281 Second,
282 Fraction(u8),
284 Meridiem { lower: bool, dots: bool },
285 Weekday,
286 WeekdayName { upper: Vec<bool>, trim: bool },
287 Era,
289 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
299const 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 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
383fn 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
394pub 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
441fn 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
448pub 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
463pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
479pub enum Reading {
480 Days,
481 Seconds,
482}
483
484fn 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
513fn 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
533pub 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
637pub 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;