1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_charset::Codepage;
25use copybook_error::{Error, ErrorCode, Result};
26use std::convert::TryFrom;
27
28#[cfg(test)]
29const DEFAULT_PROPTEST_CASE_COUNT: u32 = 512;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ZeroSignPolicy {
47 Positive,
49 Preferred,
51}
52
53static ASCII_POSITIVE_OVERPUNCH: [u8; 10] = [
58 b'{', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', ];
69
70static ASCII_NEGATIVE_OVERPUNCH: [u8; 10] = [
73 b'}', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', ];
84
85static ASCII_OVERPUNCH_DECODE: [Option<(u8, bool)>; 256] = {
88 let mut table = [None; 256];
89
90 table[b'{' as usize] = Some((0, false));
92 table[b'A' as usize] = Some((1, false));
93 table[b'B' as usize] = Some((2, false));
94 table[b'C' as usize] = Some((3, false));
95 table[b'D' as usize] = Some((4, false));
96 table[b'E' as usize] = Some((5, false));
97 table[b'F' as usize] = Some((6, false));
98 table[b'G' as usize] = Some((7, false));
99 table[b'H' as usize] = Some((8, false));
100 table[b'I' as usize] = Some((9, false));
101
102 table[b'}' as usize] = Some((0, true));
104 table[b'J' as usize] = Some((1, true));
105 table[b'K' as usize] = Some((2, true));
106 table[b'L' as usize] = Some((3, true));
107 table[b'M' as usize] = Some((4, true));
108 table[b'N' as usize] = Some((5, true));
109 table[b'O' as usize] = Some((6, true));
110 table[b'P' as usize] = Some((7, true));
111 table[b'Q' as usize] = Some((8, true));
112 table[b'R' as usize] = Some((9, true));
113
114 table[b'0' as usize] = Some((0, false));
116 table[b'1' as usize] = Some((1, false));
117 table[b'2' as usize] = Some((2, false));
118 table[b'3' as usize] = Some((3, false));
119 table[b'4' as usize] = Some((4, false));
120 table[b'5' as usize] = Some((5, false));
121 table[b'6' as usize] = Some((6, false));
122 table[b'7' as usize] = Some((7, false));
123 table[b'8' as usize] = Some((8, false));
124 table[b'9' as usize] = Some((9, false));
125
126 table
127};
128
129#[must_use]
133#[inline]
134pub fn encode_ebcdic_overpunch_zone(digit: u8, is_negative: bool, policy: ZeroSignPolicy) -> u8 {
135 debug_assert!(digit <= 9, "Digit must be 0-9");
136
137 if digit == 0 && policy == ZeroSignPolicy::Preferred {
138 return 0xF; }
140
141 if is_negative {
142 0xD } else {
144 0xC }
146}
147
148#[must_use]
152#[inline]
153pub const fn decode_ebcdic_overpunch_zone(zone: u8) -> Option<(bool, bool)> {
154 match zone {
155 0xC | 0xF => Some((true, false)), 0xD => Some((true, true)), _ => None, }
159}
160
161#[inline]
166#[must_use = "Handle the Result or propagate the error"]
167pub fn encode_overpunch_byte(
168 digit: u8,
169 is_negative: bool,
170 codepage: Codepage,
171 policy: ZeroSignPolicy,
172) -> Result<u8> {
173 if digit > 9 {
174 return Err(Error::new(
175 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
176 format!("Invalid digit {digit} for overpunch encoding"),
177 ));
178 }
179
180 if codepage == Codepage::ASCII {
181 if digit <= 9 {
183 let byte_value = if is_negative {
184 ASCII_NEGATIVE_OVERPUNCH[digit as usize]
185 } else {
186 ASCII_POSITIVE_OVERPUNCH[digit as usize]
187 };
188 Ok(byte_value)
189 } else {
190 Err(Error::new(
191 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
192 format!("Invalid digit {digit} for ASCII overpunch encoding"),
193 ))
194 }
195 } else {
196 let zone = encode_ebcdic_overpunch_zone(digit, is_negative, policy);
198 Ok((zone << 4) | digit)
199 }
200}
201
202#[inline]
207#[must_use = "Handle the Result or propagate the error"]
208pub fn decode_overpunch_byte(byte: u8, codepage: Codepage) -> Result<(u8, bool)> {
209 if codepage == Codepage::ASCII {
210 if let Some((digit, is_negative)) = ASCII_OVERPUNCH_DECODE[byte as usize] {
211 Ok((digit, is_negative))
212 } else {
213 Err(Error::new(
214 ErrorCode::CBKD411_ZONED_BAD_SIGN,
215 format!("Invalid ASCII overpunch byte 0x{byte:02X}"),
216 ))
217 }
218 } else {
219 let zone = (byte >> 4) & 0x0F;
221 let digit = byte & 0x0F;
222
223 if digit > 9 {
224 return Err(Error::new(
225 ErrorCode::CBKD411_ZONED_BAD_SIGN,
226 format!("Invalid digit nibble 0x{digit:X} in EBCDIC overpunch byte 0x{byte:02X}"),
227 ));
228 }
229
230 if let Some((is_signed, is_negative)) = decode_ebcdic_overpunch_zone(zone) {
231 if !is_signed {
232 return Err(Error::new(
233 ErrorCode::CBKD411_ZONED_BAD_SIGN,
234 format!("Unsigned zone 0x{zone:X} in signed EBCDIC field"),
235 ));
236 }
237 Ok((digit, is_negative))
238 } else {
239 Err(Error::new(
240 ErrorCode::CBKD411_ZONED_BAD_SIGN,
241 format!("Invalid EBCDIC zone nibble 0x{zone:X} in byte 0x{byte:02X}"),
242 ))
243 }
244 }
245}
246
247#[must_use]
249#[inline]
250pub fn is_valid_overpunch(byte: u8, codepage: Codepage) -> bool {
251 if codepage == Codepage::ASCII {
252 ASCII_OVERPUNCH_DECODE[byte as usize].is_some()
253 } else {
254 let zone = (byte >> 4) & 0x0F;
255 let digit = byte & 0x0F;
256 digit <= 9 && decode_ebcdic_overpunch_zone(zone).is_some()
257 }
258}
259
260#[must_use]
262#[inline]
263pub fn get_all_valid_overpunch_bytes(codepage: Codepage) -> Vec<u8> {
264 if codepage == Codepage::ASCII {
265 ASCII_OVERPUNCH_DECODE
266 .iter()
267 .enumerate()
268 .filter_map(|(byte, mapping)| mapping.and_then(|_| u8::try_from(byte).ok()))
269 .collect()
270 } else {
271 let mut bytes = Vec::new();
272 for zone in [0xC, 0xD, 0xF] {
273 for digit in 0..=9 {
274 bytes.push((zone << 4) | digit);
275 }
276 }
277 bytes
278 }
279}
280
281#[cfg(test)]
282#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
283mod tests {
284 use super::*;
285 use proptest::prelude::*;
286 use proptest::test_runner::RngSeed;
287 use std::collections::hash_map::DefaultHasher;
288 use std::hash::{Hash, Hasher};
289
290 fn proptest_case_count() -> u32 {
291 option_env!("PROPTEST_CASES")
292 .and_then(|s| s.parse().ok())
293 .unwrap_or(DEFAULT_PROPTEST_CASE_COUNT)
294 }
295
296 fn zoned_overpunch_proptest_config() -> ProptestConfig {
297 let mut cfg = ProptestConfig {
298 cases: proptest_case_count(),
299 max_shrink_time: 0,
300 ..ProptestConfig::default()
301 };
302
303 if let Ok(seed_value) = std::env::var("PROPTEST_SEED")
304 && !seed_value.is_empty()
305 {
306 let parsed_seed = seed_value.parse::<u64>().unwrap_or_else(|_| {
307 let mut hasher = DefaultHasher::new();
308 seed_value.hash(&mut hasher);
309 hasher.finish()
310 });
311 cfg.rng_seed = RngSeed::Fixed(parsed_seed);
312 }
313
314 cfg
315 }
316
317 proptest! {
318 #![proptest_config(zoned_overpunch_proptest_config())]
319 #[test]
320 fn prop_encode_decode_parity(
321 digit in 0u8..=9,
322 is_negative in any::<bool>(),
323 codepage in prop_oneof![
324 Just(Codepage::ASCII),
325 Just(Codepage::CP037),
326 Just(Codepage::CP273),
327 Just(Codepage::CP500),
328 Just(Codepage::CP1047),
329 Just(Codepage::CP1140),
330 ],
331 policy in prop_oneof![Just(ZeroSignPolicy::Positive), Just(ZeroSignPolicy::Preferred)],
332 ) {
333 let encoded = encode_overpunch_byte(digit, is_negative, codepage, policy)
334 .expect("encoding should succeed for digits 0-9");
335
336 prop_assert!(is_valid_overpunch(encoded, codepage));
337
338 let (decoded_digit, decoded_negative) =
339 decode_overpunch_byte(encoded, codepage).expect("decode must succeed for encoded byte");
340
341 prop_assert_eq!(decoded_digit, digit);
342
343 let expected_negative = if codepage.is_ebcdic()
344 && policy == ZeroSignPolicy::Preferred
345 && digit == 0
346 {
347 false
349 } else {
350 is_negative
351 };
352
353 prop_assert_eq!(decoded_negative, expected_negative);
354 }
355 }
356
357 #[test]
358 fn test_ascii_overpunch_encode_decode() {
359 for digit in 0..=9 {
361 let encoded =
362 encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive)
363 .expect("Failed to encode positive digit");
364 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::ASCII)
365 .expect("Failed to decode positive digit");
366
367 assert_eq!(decoded_digit, digit);
368 assert!(!is_negative);
369 }
370
371 for digit in 0..=9 {
373 let encoded =
374 encode_overpunch_byte(digit, true, Codepage::ASCII, ZeroSignPolicy::Positive)
375 .expect("Failed to encode negative digit");
376 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::ASCII)
377 .expect("Failed to decode negative digit");
378
379 assert_eq!(decoded_digit, digit);
380 assert!(is_negative);
381 }
382 }
383
384 #[test]
385 fn test_ebcdic_overpunch_encode_decode() {
386 for digit in 0..=9 {
388 let encoded =
389 encode_overpunch_byte(digit, false, Codepage::CP037, ZeroSignPolicy::Positive)
390 .expect("Failed to encode positive digit");
391 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::CP037)
392 .expect("Failed to decode positive digit");
393
394 assert_eq!(decoded_digit, digit);
395 assert!(!is_negative);
396
397 let zone = (encoded >> 4) & 0x0F;
399 if digit != 0 {
400 assert_eq!(zone, 0xC);
401 }
402 }
403
404 for digit in 0..=9 {
406 let encoded =
407 encode_overpunch_byte(digit, true, Codepage::CP037, ZeroSignPolicy::Positive)
408 .expect("Failed to encode negative digit");
409 let (decoded_digit, is_negative) = decode_overpunch_byte(encoded, Codepage::CP037)
410 .expect("Failed to decode negative digit");
411
412 assert_eq!(decoded_digit, digit);
413 assert!(is_negative);
414
415 let zone = (encoded >> 4) & 0x0F;
417 assert_eq!(zone, 0xD);
418 }
419 }
420
421 #[test]
422 fn test_zero_sign_policies() {
423 let encoded_pos =
425 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Positive)
426 .expect("Failed to encode zero with positive policy");
427 let zone_pos = (encoded_pos >> 4) & 0x0F;
428 assert_eq!(zone_pos, 0xC);
429
430 let encoded_pref_pos =
432 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Preferred)
433 .expect("Failed to encode zero with preferred policy");
434 let zone_pref_pos = (encoded_pref_pos >> 4) & 0x0F;
435 assert_eq!(zone_pref_pos, 0xF);
436
437 let encoded_pref_neg =
438 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Preferred)
439 .expect("Failed to encode negative zero with preferred policy");
440 let zone_pref_neg = (encoded_pref_neg >> 4) & 0x0F;
441 assert_eq!(zone_pref_neg, 0xF);
442 }
443
444 #[test]
445 fn test_invalid_inputs() {
446 let result = encode_overpunch_byte(10, false, Codepage::ASCII, ZeroSignPolicy::Positive);
448 assert!(result.is_err());
449
450 let result = decode_overpunch_byte(0xFF, Codepage::ASCII);
452 assert!(result.is_err());
453
454 let invalid_ebcdic = 0x1F; let result = decode_overpunch_byte(invalid_ebcdic, Codepage::CP037);
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_ascii_overpunch_golden_values() {
462 assert_eq!(
464 decode_overpunch_byte(b'{', Codepage::ASCII).unwrap(),
465 (0, false)
466 ); assert_eq!(
468 decode_overpunch_byte(b'A', Codepage::ASCII).unwrap(),
469 (1, false)
470 ); assert_eq!(
472 decode_overpunch_byte(b'I', Codepage::ASCII).unwrap(),
473 (9, false)
474 ); assert_eq!(
476 decode_overpunch_byte(b'}', Codepage::ASCII).unwrap(),
477 (0, true)
478 ); assert_eq!(
480 decode_overpunch_byte(b'J', Codepage::ASCII).unwrap(),
481 (1, true)
482 ); assert_eq!(
484 decode_overpunch_byte(b'R', Codepage::ASCII).unwrap(),
485 (9, true)
486 ); }
488
489 #[test]
490 fn test_ebcdic_overpunch_golden_values() {
491 assert_eq!(
493 decode_overpunch_byte(0xC0, Codepage::CP037).unwrap(),
494 (0, false)
495 ); assert_eq!(
497 decode_overpunch_byte(0xC1, Codepage::CP037).unwrap(),
498 (1, false)
499 ); assert_eq!(
501 decode_overpunch_byte(0xC9, Codepage::CP037).unwrap(),
502 (9, false)
503 ); assert_eq!(
505 decode_overpunch_byte(0xD0, Codepage::CP037).unwrap(),
506 (0, true)
507 ); assert_eq!(
509 decode_overpunch_byte(0xD1, Codepage::CP037).unwrap(),
510 (1, true)
511 ); assert_eq!(
513 decode_overpunch_byte(0xD9, Codepage::CP037).unwrap(),
514 (9, true)
515 ); assert_eq!(
517 decode_overpunch_byte(0xF0, Codepage::CP037).unwrap(),
518 (0, false)
519 ); }
521
522 #[test]
523 fn test_is_valid_overpunch() {
524 assert!(is_valid_overpunch(b'A', Codepage::ASCII));
526 assert!(is_valid_overpunch(b'J', Codepage::ASCII));
527 assert!(is_valid_overpunch(b'{', Codepage::ASCII));
528 assert!(is_valid_overpunch(b'0', Codepage::ASCII)); assert!(!is_valid_overpunch(b'Z', Codepage::ASCII));
530
531 assert!(is_valid_overpunch(0xC0, Codepage::CP037));
533 assert!(is_valid_overpunch(0xD9, Codepage::CP037));
534 assert!(is_valid_overpunch(0xF5, Codepage::CP037));
535 assert!(!is_valid_overpunch(0x1A, Codepage::CP037)); assert!(!is_valid_overpunch(0xCA, Codepage::CP037)); }
538
539 #[test]
540 fn test_get_all_valid_overpunch_bytes() {
541 let ascii_bytes = get_all_valid_overpunch_bytes(Codepage::ASCII);
542 assert!(ascii_bytes.len() >= 30); assert!(ascii_bytes.contains(&b'A'));
544 assert!(ascii_bytes.contains(&b'J'));
545 assert!(ascii_bytes.contains(&b'0'));
546
547 let ebcdic_bytes = get_all_valid_overpunch_bytes(Codepage::CP037);
548 assert_eq!(ebcdic_bytes.len(), 30); assert!(ebcdic_bytes.contains(&0xC0));
550 assert!(ebcdic_bytes.contains(&0xD9));
551 assert!(ebcdic_bytes.contains(&0xF5));
552 }
553
554 #[test]
557 fn test_ascii_decode_each_positive_overpunch_char() {
558 let expected: [(u8, u8); 10] = [
559 (b'{', 0),
560 (b'A', 1),
561 (b'B', 2),
562 (b'C', 3),
563 (b'D', 4),
564 (b'E', 5),
565 (b'F', 6),
566 (b'G', 7),
567 (b'H', 8),
568 (b'I', 9),
569 ];
570 for (byte, digit) in expected {
571 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
572 assert_eq!(d, digit, "positive overpunch char 0x{byte:02X}");
573 assert!(
574 !neg,
575 "positive overpunch char 0x{byte:02X} should not be negative"
576 );
577 }
578 }
579
580 #[test]
581 fn test_ascii_decode_each_negative_overpunch_char() {
582 let expected: [(u8, u8); 10] = [
583 (b'}', 0),
584 (b'J', 1),
585 (b'K', 2),
586 (b'L', 3),
587 (b'M', 4),
588 (b'N', 5),
589 (b'O', 6),
590 (b'P', 7),
591 (b'Q', 8),
592 (b'R', 9),
593 ];
594 for (byte, digit) in expected {
595 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
596 assert_eq!(d, digit, "negative overpunch char 0x{byte:02X}");
597 assert!(
598 neg,
599 "negative overpunch char 0x{byte:02X} should be negative"
600 );
601 }
602 }
603
604 #[test]
605 fn test_ascii_encode_each_digit_positive() {
606 let expected_bytes: [u8; 10] = *b"{ABCDEFGHI";
607 for digit in 0u8..=9 {
608 let enc =
609 encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive)
610 .unwrap();
611 assert_eq!(
612 enc, expected_bytes[digit as usize],
613 "encode positive digit {digit}"
614 );
615 }
616 }
617
618 #[test]
619 fn test_ascii_encode_each_digit_negative() {
620 let expected_bytes: [u8; 10] = *b"}JKLMNOPQR";
621 for digit in 0u8..=9 {
622 let enc = encode_overpunch_byte(digit, true, Codepage::ASCII, ZeroSignPolicy::Positive)
623 .unwrap();
624 assert_eq!(
625 enc, expected_bytes[digit as usize],
626 "encode negative digit {digit}"
627 );
628 }
629 }
630
631 #[test]
632 fn test_all_ebcdic_codepage_variants_encode_decode() -> std::result::Result<(), String> {
633 let ebcdic_codepages = [
634 Codepage::CP037,
635 Codepage::CP273,
636 Codepage::CP500,
637 Codepage::CP1047,
638 Codepage::CP1140,
639 ];
640 for cp in ebcdic_codepages {
641 for digit in 0u8..=9 {
642 for is_negative in [false, true] {
643 let enc =
644 encode_overpunch_byte(digit, is_negative, cp, ZeroSignPolicy::Positive)
645 .map_err(|error| {
646 format!("{cp:?} digit={digit} neg={is_negative}: {error}")
647 })?;
648 let (d, neg) = decode_overpunch_byte(enc, cp)
649 .map_err(|error| format!("{cp:?} byte=0x{enc:02X}: {error}"))?;
650 assert_eq!(d, digit, "{cp:?} digit={digit} neg={is_negative}");
651 assert_eq!(neg, is_negative, "{cp:?} digit={digit} neg={is_negative}");
652 }
653 }
654 }
655 Ok(())
656 }
657
658 #[test]
659 fn test_ebcdic_zone_nibbles_all_codepages() {
660 let ebcdic_codepages = [
661 Codepage::CP037,
662 Codepage::CP273,
663 Codepage::CP500,
664 Codepage::CP1047,
665 Codepage::CP1140,
666 ];
667 for cp in ebcdic_codepages {
668 for digit in 0u8..=9 {
669 let pos =
670 encode_overpunch_byte(digit, false, cp, ZeroSignPolicy::Positive).unwrap();
671 assert_eq!(
672 (pos >> 4) & 0x0F,
673 0xC,
674 "{cp:?} positive zone for digit {digit}"
675 );
676 assert_eq!(
677 pos & 0x0F,
678 digit,
679 "{cp:?} positive digit nibble for {digit}"
680 );
681
682 let neg = encode_overpunch_byte(digit, true, cp, ZeroSignPolicy::Positive).unwrap();
683 assert_eq!(
684 (neg >> 4) & 0x0F,
685 0xD,
686 "{cp:?} negative zone for digit {digit}"
687 );
688 assert_eq!(
689 neg & 0x0F,
690 digit,
691 "{cp:?} negative digit nibble for {digit}"
692 );
693 }
694 }
695 }
696
697 #[test]
698 fn test_invalid_ascii_overpunch_bytes() {
699 let invalid_bytes: [u8; 10] = [b'S', b'T', b'Z', b'a', b'z', b'!', b'@', b'#', 0x00, 0xFF];
700 for byte in invalid_bytes {
701 let result = decode_overpunch_byte(byte, Codepage::ASCII);
702 assert!(
703 result.is_err(),
704 "byte 0x{byte:02X} should be invalid ASCII overpunch"
705 );
706 assert!(!is_valid_overpunch(byte, Codepage::ASCII));
707 }
708 }
709
710 #[test]
711 fn test_invalid_ebcdic_overpunch_bytes() {
712 let invalid_zones: [u8; 6] = [0x0, 0x1, 0x2, 0xA, 0xB, 0xE];
714 for zone in invalid_zones {
715 let byte = (zone << 4) | 0x05; let result = decode_overpunch_byte(byte, Codepage::CP037);
717 assert!(
718 result.is_err(),
719 "zone 0x{zone:X} should be invalid for EBCDIC"
720 );
721 assert!(!is_valid_overpunch(byte, Codepage::CP037));
722 }
723 for digit_nibble in 0xAu8..=0xF {
725 let byte = (0xC << 4) | digit_nibble; let result = decode_overpunch_byte(byte, Codepage::CP037);
727 assert!(
728 result.is_err(),
729 "digit nibble 0x{digit_nibble:X} should be invalid"
730 );
731 assert!(!is_valid_overpunch(byte, Codepage::CP037));
732 }
733 }
734
735 #[test]
736 fn test_encode_invalid_digit_rejected() {
737 for digit in [10u8, 11, 15, 99, 255] {
738 let r1 = encode_overpunch_byte(digit, false, Codepage::ASCII, ZeroSignPolicy::Positive);
739 assert!(r1.is_err(), "digit {digit} should be rejected for ASCII");
740 let r2 = encode_overpunch_byte(digit, false, Codepage::CP037, ZeroSignPolicy::Positive);
741 assert!(r2.is_err(), "digit {digit} should be rejected for EBCDIC");
742 }
743 }
744
745 #[test]
746 fn test_ascii_round_trip_all_digit_sign_combinations() {
747 for digit in 0u8..=9 {
748 for is_negative in [false, true] {
749 let enc = encode_overpunch_byte(
750 digit,
751 is_negative,
752 Codepage::ASCII,
753 ZeroSignPolicy::Positive,
754 )
755 .unwrap();
756 let (d, neg) = decode_overpunch_byte(enc, Codepage::ASCII).unwrap();
757 assert_eq!(d, digit);
758 assert_eq!(neg, is_negative);
759 }
760 }
761 }
762
763 #[test]
764 fn test_ebcdic_preferred_zero_round_trip_all_codepages() {
765 let ebcdic_codepages = [
766 Codepage::CP037,
767 Codepage::CP273,
768 Codepage::CP500,
769 Codepage::CP1047,
770 Codepage::CP1140,
771 ];
772 for cp in ebcdic_codepages {
773 let enc_pos = encode_overpunch_byte(0, false, cp, ZeroSignPolicy::Preferred).unwrap();
775 let enc_neg = encode_overpunch_byte(0, true, cp, ZeroSignPolicy::Preferred).unwrap();
776 assert_eq!(enc_pos, 0xF0, "{cp:?} preferred +0");
777 assert_eq!(enc_neg, 0xF0, "{cp:?} preferred -0");
778
779 let (d_pos, neg_pos) = decode_overpunch_byte(enc_pos, cp).unwrap();
780 assert_eq!(d_pos, 0);
781 assert!(!neg_pos, "{cp:?} preferred zero decodes as positive");
782
783 for digit in 1u8..=9 {
785 let enc =
786 encode_overpunch_byte(digit, true, cp, ZeroSignPolicy::Preferred).unwrap();
787 let zone = (enc >> 4) & 0x0F;
788 assert_eq!(
789 zone, 0xD,
790 "{cp:?} preferred policy should not affect digit {digit}"
791 );
792 }
793 }
794 }
795
796 #[test]
797 fn test_positive_and_negative_zero_ascii() {
798 let enc_pos =
800 encode_overpunch_byte(0, false, Codepage::ASCII, ZeroSignPolicy::Positive).unwrap();
801 assert_eq!(enc_pos, b'{');
802 let (d, neg) = decode_overpunch_byte(enc_pos, Codepage::ASCII).unwrap();
803 assert_eq!(d, 0);
804 assert!(!neg);
805
806 let enc_neg =
808 encode_overpunch_byte(0, true, Codepage::ASCII, ZeroSignPolicy::Positive).unwrap();
809 assert_eq!(enc_neg, b'}');
810 let (d, neg) = decode_overpunch_byte(enc_neg, Codepage::ASCII).unwrap();
811 assert_eq!(d, 0);
812 assert!(neg);
813 }
814
815 #[test]
816 fn test_positive_and_negative_zero_ebcdic() {
817 let enc =
819 encode_overpunch_byte(0, false, Codepage::CP037, ZeroSignPolicy::Positive).unwrap();
820 assert_eq!(enc, 0xC0);
821 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
822 assert_eq!(d, 0);
823 assert!(!neg);
824
825 let enc =
827 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Positive).unwrap();
828 assert_eq!(enc, 0xD0);
829 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
830 assert_eq!(d, 0);
831 assert!(neg);
832
833 let enc =
835 encode_overpunch_byte(0, true, Codepage::CP037, ZeroSignPolicy::Preferred).unwrap();
836 assert_eq!(enc, 0xF0);
837 let (d, neg) = decode_overpunch_byte(enc, Codepage::CP037).unwrap();
838 assert_eq!(d, 0);
839 assert!(!neg, "preferred zero always decodes as positive");
840 }
841
842 #[test]
843 fn test_ascii_regular_digits_decode_as_unsigned_positive() {
844 for digit in 0u8..=9 {
846 let byte = b'0' + digit;
847 let (d, neg) = decode_overpunch_byte(byte, Codepage::ASCII).unwrap();
848 assert_eq!(d, digit);
849 assert!(!neg, "plain digit {digit} should decode as positive");
850 assert!(is_valid_overpunch(byte, Codepage::ASCII));
851 }
852 }
853
854 #[test]
855 fn test_ebcdic_f_zone_digits_decode_as_positive() {
856 for digit in 0u8..=9 {
858 let byte = 0xF0 | digit;
859 let (d, neg) = decode_overpunch_byte(byte, Codepage::CP037).unwrap();
860 assert_eq!(d, digit);
861 assert!(!neg, "F-zone digit {digit} should decode as positive");
862 }
863 }
864
865 #[test]
866 fn test_boundary_single_digit_encode_decode() {
867 for &(digit, is_neg) in &[(0u8, false), (0, true), (9, false), (9, true)] {
869 let ascii_enc =
870 encode_overpunch_byte(digit, is_neg, Codepage::ASCII, ZeroSignPolicy::Positive)
871 .unwrap();
872 let (d, n) = decode_overpunch_byte(ascii_enc, Codepage::ASCII).unwrap();
873 assert_eq!(d, digit);
874 assert_eq!(n, is_neg);
875
876 let ebcdic_enc =
877 encode_overpunch_byte(digit, is_neg, Codepage::CP037, ZeroSignPolicy::Positive)
878 .unwrap();
879 let (d, n) = decode_overpunch_byte(ebcdic_enc, Codepage::CP037).unwrap();
880 assert_eq!(d, digit);
881 assert_eq!(n, is_neg);
882 }
883 }
884
885 #[test]
886 fn test_ascii_preferred_zero_policy_has_no_effect() {
887 let pos =
889 encode_overpunch_byte(0, false, Codepage::ASCII, ZeroSignPolicy::Preferred).unwrap();
890 let neg =
891 encode_overpunch_byte(0, true, Codepage::ASCII, ZeroSignPolicy::Preferred).unwrap();
892 assert_eq!(pos, b'{', "ASCII preferred +0 should still be '{{' ");
893 assert_eq!(neg, b'}', "ASCII preferred -0 should still be '}}' ");
894 }
895}