Skip to main content

copybook_codepage/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Codepage domain types and helpers.
4//!
5//! This crate contains codepage-related enums and codepage-specific constants
6//! used by charset and numeric handling.
7
8#[cfg(feature = "clap")]
9use clap::ValueEnum;
10use serde::{Deserialize, Serialize};
11use std::str::FromStr;
12
13/// Character encoding specification
14///
15/// # Examples
16///
17/// ```
18/// use copybook_codepage::Codepage;
19///
20/// let cp = Codepage::CP037;
21/// assert!(cp.is_ebcdic());
22/// assert_eq!(cp.code_page_number(), Some(37));
23/// assert_eq!(cp.description(), "EBCDIC Code Page 037 (US/Canada)");
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "clap", derive(ValueEnum))]
27pub enum Codepage {
28    /// ASCII encoding
29    ASCII,
30    /// EBCDIC Code Page 037 (US/Canada)
31    #[cfg_attr(feature = "clap", value(name = "cp037"))]
32    CP037,
33    /// EBCDIC Code Page 273 (Germany/Austria)
34    #[cfg_attr(feature = "clap", value(name = "cp273"))]
35    CP273,
36    /// EBCDIC Code Page 500 (International)
37    #[cfg_attr(feature = "clap", value(name = "cp500"))]
38    CP500,
39    /// EBCDIC Code Page 1047 (Open Systems)
40    #[cfg_attr(feature = "clap", value(name = "cp1047"))]
41    CP1047,
42    /// EBCDIC Code Page 1140 (US/Canada with Euro)
43    #[cfg_attr(feature = "clap", value(name = "cp1140"))]
44    CP1140,
45}
46
47#[derive(Debug, Clone, Copy)]
48struct CodepageMetadata {
49    display_name: &'static str,
50    description: &'static str,
51    code_page_number: Option<u16>,
52}
53
54impl CodepageMetadata {
55    const ASCII: Self = Self {
56        display_name: "ascii",
57        description: "ASCII encoding",
58        code_page_number: None,
59    };
60
61    const fn ebcdic(
62        display_name: &'static str,
63        description: &'static str,
64        code_page_number: u16,
65    ) -> Self {
66        Self {
67            display_name,
68            description,
69            code_page_number: Some(code_page_number),
70        }
71    }
72}
73
74impl Codepage {
75    const METADATA: [(Self, CodepageMetadata); 6] = [
76        (Self::ASCII, CodepageMetadata::ASCII),
77        (
78            Self::CP037,
79            CodepageMetadata::ebcdic("cp037", "EBCDIC Code Page 037 (US/Canada)", 37),
80        ),
81        (
82            Self::CP273,
83            CodepageMetadata::ebcdic("cp273", "EBCDIC Code Page 273 (Germany/Austria)", 273),
84        ),
85        (
86            Self::CP500,
87            CodepageMetadata::ebcdic("cp500", "EBCDIC Code Page 500 (International)", 500),
88        ),
89        (
90            Self::CP1047,
91            CodepageMetadata::ebcdic("cp1047", "EBCDIC Code Page 1047 (Open Systems)", 1047),
92        ),
93        (
94            Self::CP1140,
95            CodepageMetadata::ebcdic(
96                "cp1140",
97                "EBCDIC Code Page 1140 (US/Canada with Euro)",
98                1140,
99            ),
100        ),
101    ];
102
103    const fn metadata(self) -> CodepageMetadata {
104        match self {
105            Self::ASCII => Self::METADATA[0].1,
106            Self::CP037 => Self::METADATA[1].1,
107            Self::CP273 => Self::METADATA[2].1,
108            Self::CP500 => Self::METADATA[3].1,
109            Self::CP1047 => Self::METADATA[4].1,
110            Self::CP1140 => Self::METADATA[5].1,
111        }
112    }
113
114    /// Check if this is an ASCII codepage
115    #[must_use]
116    #[inline]
117    pub const fn is_ascii(self) -> bool {
118        self.metadata().code_page_number.is_none()
119    }
120
121    /// Check if this is an EBCDIC codepage
122    #[must_use]
123    #[inline]
124    pub const fn is_ebcdic(self) -> bool {
125        !self.is_ascii()
126    }
127
128    /// Get the numeric code page identifier
129    #[must_use]
130    #[inline]
131    pub const fn code_page_number(self) -> Option<u16> {
132        self.metadata().code_page_number
133    }
134
135    /// Get a human-readable description of the codepage
136    #[must_use]
137    #[inline]
138    pub const fn description(self) -> &'static str {
139        self.metadata().description
140    }
141
142    /// Get the canonical lower-case command-line/display spelling.
143    #[must_use]
144    #[inline]
145    pub const fn as_str(self) -> &'static str {
146        self.metadata().display_name
147    }
148}
149
150impl std::fmt::Display for Codepage {
151    #[inline]
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.write_str(self.as_str())
154    }
155}
156
157impl FromStr for Codepage {
158    type Err = std::convert::Infallible;
159
160    #[inline]
161    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
162        Ok(Self::METADATA
163            .iter()
164            .find_map(|(codepage, metadata)| {
165                metadata
166                    .display_name
167                    .eq_ignore_ascii_case(s)
168                    .then_some(*codepage)
169            })
170            // Default to CP037 for backward compatibility
171            .unwrap_or(Self::CP037))
172    }
173}
174
175/// Policy for handling unmappable characters during decode
176///
177/// # Examples
178///
179/// ```
180/// use copybook_codepage::UnmappablePolicy;
181///
182/// let policy = UnmappablePolicy::Replace;
183/// assert_eq!(format!("{policy}"), "replace");
184/// ```
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[cfg_attr(feature = "clap", derive(ValueEnum))]
187pub enum UnmappablePolicy {
188    /// Error on unmappable characters
189    #[cfg_attr(feature = "clap", value(name = "error"))]
190    Error,
191    /// Replace with U+FFFD
192    #[cfg_attr(feature = "clap", value(name = "replace"))]
193    Replace,
194    /// Skip unmappable characters
195    #[cfg_attr(feature = "clap", value(name = "skip"))]
196    Skip,
197}
198
199impl UnmappablePolicy {
200    const NAMES: [(Self, &'static str); 3] = [
201        (Self::Error, "error"),
202        (Self::Replace, "replace"),
203        (Self::Skip, "skip"),
204    ];
205
206    /// Get the canonical lower-case command-line/display spelling.
207    #[must_use]
208    #[inline]
209    pub const fn as_str(self) -> &'static str {
210        match self {
211            Self::Error => Self::NAMES[0].1,
212            Self::Replace => Self::NAMES[1].1,
213            Self::Skip => Self::NAMES[2].1,
214        }
215    }
216}
217
218impl std::fmt::Display for UnmappablePolicy {
219    #[inline]
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        f.write_str(self.as_str())
222    }
223}
224
225impl FromStr for UnmappablePolicy {
226    type Err = std::convert::Infallible;
227
228    #[inline]
229    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
230        Ok(Self::NAMES
231            .iter()
232            .find_map(|(policy, name)| name.eq_ignore_ascii_case(s).then_some(*policy))
233            // Default to Error for backward compatibility
234            .unwrap_or(Self::Error))
235    }
236}
237
238// Zoned decimal sign tables map the zone nibble (high 4 bits) to sign info.
239static EBCDIC_ZONED_SIGNS: [(bool, bool); 16] = [
240    (false, false), // 0x0_: unsigned
241    (false, false), // 0x1_: unsigned
242    (false, false), // 0x2_: unsigned
243    (false, false), // 0x3_: unsigned
244    (false, false), // 0x4_: unsigned
245    (false, false), // 0x5_: unsigned
246    (false, false), // 0x6_: unsigned
247    (false, false), // 0x7_: unsigned
248    (false, false), // 0x8_: unsigned
249    (false, false), // 0x9_: unsigned
250    (false, false), // 0xA_: unsigned
251    (false, false), // 0xB_: unsigned
252    (true, false),  // 0xC_: positive
253    (true, true),   // 0xD_: negative
254    (false, false), // 0xE_: unsigned
255    (true, false),  // 0xF_: positive (default)
256];
257
258// ASCII overpunch requires byte-level logic, so zoned table is intentionally
259// unsigned to avoid accidental misuse in ASCII code paths.
260static ASCII_ZONED_SIGNS: [(bool, bool); 16] = [(false, false); 16];
261
262/// Get zoned decimal sign table for a codepage.
263#[must_use]
264#[inline]
265pub fn get_zoned_sign_table(codepage: Codepage) -> &'static [(bool, bool); 16] {
266    match codepage {
267        Codepage::ASCII => &ASCII_ZONED_SIGNS,
268        _ => &EBCDIC_ZONED_SIGNS,
269    }
270}
271
272/// Get the space byte value for a codepage.
273///
274/// Returns `0x20` for ASCII, `0x40` for all EBCDIC codepages.
275#[must_use]
276#[inline]
277pub const fn space_byte(codepage: Codepage) -> u8 {
278    match codepage {
279        Codepage::ASCII => 0x20,
280        Codepage::CP037
281        | Codepage::CP273
282        | Codepage::CP500
283        | Codepage::CP1047
284        | Codepage::CP1140 => 0x40,
285    }
286}
287
288#[cfg(test)]
289#[allow(clippy::expect_used, clippy::unwrap_used)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_space_byte_ascii() {
295        assert_eq!(space_byte(Codepage::ASCII), 0x20);
296    }
297
298    #[test]
299    fn test_space_byte_ebcdic() {
300        assert_eq!(space_byte(Codepage::CP037), 0x40);
301        assert_eq!(space_byte(Codepage::CP273), 0x40);
302        assert_eq!(space_byte(Codepage::CP500), 0x40);
303        assert_eq!(space_byte(Codepage::CP1047), 0x40);
304        assert_eq!(space_byte(Codepage::CP1140), 0x40);
305    }
306
307    #[test]
308    fn test_codepage_is_ascii() {
309        assert!(Codepage::ASCII.is_ascii());
310        assert!(!Codepage::CP037.is_ascii());
311    }
312
313    #[test]
314    fn test_codepage_is_ebcdic() {
315        assert!(!Codepage::ASCII.is_ebcdic());
316        assert!(Codepage::CP037.is_ebcdic());
317    }
318
319    #[test]
320    fn test_codepage_code_page_number() {
321        assert_eq!(Codepage::ASCII.code_page_number(), None);
322        assert_eq!(Codepage::CP037.code_page_number(), Some(37));
323        assert_eq!(Codepage::CP1140.code_page_number(), Some(1140));
324    }
325
326    #[test]
327    fn test_codepage_from_str_defaults_to_cp037() {
328        assert_eq!(
329            <Codepage as std::str::FromStr>::from_str("unknown").unwrap(),
330            Codepage::CP037
331        );
332    }
333
334    #[test]
335    fn test_unmappable_policy_from_str_defaults_to_error() {
336        assert_eq!(
337            <UnmappablePolicy as std::str::FromStr>::from_str("unknown").unwrap(),
338            UnmappablePolicy::Error
339        );
340    }
341
342    #[test]
343    fn test_get_zoned_sign_table_ascii_is_unsigned() {
344        let table = get_zoned_sign_table(Codepage::ASCII);
345        assert!(table.iter().all(|entry| *entry == (false, false)));
346    }
347
348    #[test]
349    fn test_get_zoned_sign_table_ebcdic_has_signed_entries() {
350        let table = get_zoned_sign_table(Codepage::CP037);
351        assert_eq!(table[0xC], (true, false));
352        assert_eq!(table[0xD], (true, true));
353        assert_eq!(table[0xF], (true, false));
354    }
355
356    // --- Codepage::description tests ---
357
358    #[test]
359    fn test_codepage_description_all_variants() {
360        assert_eq!(Codepage::ASCII.description(), "ASCII encoding");
361        assert_eq!(
362            Codepage::CP037.description(),
363            "EBCDIC Code Page 037 (US/Canada)"
364        );
365        assert_eq!(
366            Codepage::CP273.description(),
367            "EBCDIC Code Page 273 (Germany/Austria)"
368        );
369        assert_eq!(
370            Codepage::CP500.description(),
371            "EBCDIC Code Page 500 (International)"
372        );
373        assert_eq!(
374            Codepage::CP1047.description(),
375            "EBCDIC Code Page 1047 (Open Systems)"
376        );
377        assert_eq!(
378            Codepage::CP1140.description(),
379            "EBCDIC Code Page 1140 (US/Canada with Euro)"
380        );
381    }
382
383    // --- Codepage Display tests ---
384
385    #[test]
386    fn test_codepage_display_all_variants() {
387        assert_eq!(format!("{}", Codepage::ASCII), "ascii");
388        assert_eq!(format!("{}", Codepage::CP037), "cp037");
389        assert_eq!(format!("{}", Codepage::CP273), "cp273");
390        assert_eq!(format!("{}", Codepage::CP500), "cp500");
391        assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
392        assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
393    }
394
395    // --- Codepage FromStr tests ---
396
397    #[test]
398    fn test_codepage_from_str_all_valid_variants() {
399        assert_eq!(
400            <Codepage as std::str::FromStr>::from_str("ascii").unwrap(),
401            Codepage::ASCII
402        );
403        assert_eq!(
404            <Codepage as std::str::FromStr>::from_str("cp273").unwrap(),
405            Codepage::CP273
406        );
407        assert_eq!(
408            <Codepage as std::str::FromStr>::from_str("cp500").unwrap(),
409            Codepage::CP500
410        );
411        assert_eq!(
412            <Codepage as std::str::FromStr>::from_str("cp1047").unwrap(),
413            Codepage::CP1047
414        );
415        assert_eq!(
416            <Codepage as std::str::FromStr>::from_str("cp1140").unwrap(),
417            Codepage::CP1140
418        );
419    }
420
421    #[test]
422    fn test_codepage_from_str_case_insensitive() {
423        assert_eq!(
424            <Codepage as std::str::FromStr>::from_str("ASCII").unwrap(),
425            Codepage::ASCII
426        );
427        assert_eq!(
428            <Codepage as std::str::FromStr>::from_str("CP273").unwrap(),
429            Codepage::CP273
430        );
431        assert_eq!(
432            <Codepage as std::str::FromStr>::from_str("Cp500").unwrap(),
433            Codepage::CP500
434        );
435    }
436
437    #[test]
438    fn test_codepage_from_str_empty_string_defaults_to_cp037() {
439        assert_eq!(
440            <Codepage as std::str::FromStr>::from_str("").unwrap(),
441            Codepage::CP037
442        );
443    }
444
445    // --- Codepage is_ebcdic exhaustive ---
446
447    #[test]
448    fn test_codepage_is_ebcdic_all_variants() {
449        assert!(!Codepage::ASCII.is_ebcdic());
450        assert!(Codepage::CP037.is_ebcdic());
451        assert!(Codepage::CP273.is_ebcdic());
452        assert!(Codepage::CP500.is_ebcdic());
453        assert!(Codepage::CP1047.is_ebcdic());
454        assert!(Codepage::CP1140.is_ebcdic());
455    }
456
457    // --- Codepage code_page_number exhaustive ---
458
459    #[test]
460    fn test_codepage_code_page_number_all_variants() {
461        assert_eq!(Codepage::ASCII.code_page_number(), None);
462        assert_eq!(Codepage::CP037.code_page_number(), Some(37));
463        assert_eq!(Codepage::CP273.code_page_number(), Some(273));
464        assert_eq!(Codepage::CP500.code_page_number(), Some(500));
465        assert_eq!(Codepage::CP1047.code_page_number(), Some(1047));
466        assert_eq!(Codepage::CP1140.code_page_number(), Some(1140));
467    }
468
469    // --- UnmappablePolicy Display tests ---
470
471    #[test]
472    fn test_unmappable_policy_display_all_variants() {
473        assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
474        assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
475        assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
476    }
477
478    // --- UnmappablePolicy FromStr tests ---
479
480    #[test]
481    fn test_unmappable_policy_from_str_all_valid() {
482        assert_eq!(
483            <UnmappablePolicy as std::str::FromStr>::from_str("replace").unwrap(),
484            UnmappablePolicy::Replace
485        );
486        assert_eq!(
487            <UnmappablePolicy as std::str::FromStr>::from_str("skip").unwrap(),
488            UnmappablePolicy::Skip
489        );
490        assert_eq!(
491            <UnmappablePolicy as std::str::FromStr>::from_str("error").unwrap(),
492            UnmappablePolicy::Error
493        );
494    }
495
496    #[test]
497    fn test_unmappable_policy_from_str_case_insensitive() {
498        assert_eq!(
499            <UnmappablePolicy as std::str::FromStr>::from_str("REPLACE").unwrap(),
500            UnmappablePolicy::Replace
501        );
502        assert_eq!(
503            <UnmappablePolicy as std::str::FromStr>::from_str("SKIP").unwrap(),
504            UnmappablePolicy::Skip
505        );
506    }
507
508    // --- Zoned sign table exhaustive ---
509
510    #[test]
511    fn test_get_zoned_sign_table_ebcdic_unsigned_nibbles() {
512        let table = get_zoned_sign_table(Codepage::CP037);
513        for (i, &entry) in table.iter().enumerate().take(0xB + 1) {
514            assert_eq!(entry, (false, false), "Expected unsigned at nibble 0x{i:X}");
515        }
516        assert_eq!(table[0xE], (false, false));
517    }
518
519    #[test]
520    fn test_get_zoned_sign_table_all_ebcdic_codepages_same() {
521        let cp037 = get_zoned_sign_table(Codepage::CP037);
522        let cp273 = get_zoned_sign_table(Codepage::CP273);
523        let cp500 = get_zoned_sign_table(Codepage::CP500);
524        let cp1047 = get_zoned_sign_table(Codepage::CP1047);
525        let cp1140 = get_zoned_sign_table(Codepage::CP1140);
526        assert_eq!(cp037, cp273);
527        assert_eq!(cp037, cp500);
528        assert_eq!(cp037, cp1047);
529        assert_eq!(cp037, cp1140);
530    }
531
532    // --- Serde round-trip ---
533
534    #[test]
535    fn test_codepage_serde_roundtrip() {
536        let cp = Codepage::CP037;
537        let json = serde_json::to_string(&cp).unwrap();
538        let deserialized: Codepage = serde_json::from_str(&json).unwrap();
539        assert_eq!(cp, deserialized);
540    }
541
542    #[test]
543    fn test_unmappable_policy_serde_roundtrip() {
544        let policy = UnmappablePolicy::Replace;
545        let json = serde_json::to_string(&policy).unwrap();
546        let deserialized: UnmappablePolicy = serde_json::from_str(&json).unwrap();
547        assert_eq!(policy, deserialized);
548    }
549
550    // --- Additional coverage ---
551
552    #[test]
553    fn test_codepage_clone_preserves_value() {
554        let cp = Codepage::CP500;
555        let cloned = cp;
556        assert_eq!(cp, cloned);
557    }
558
559    #[test]
560    fn test_codepage_eq_different_variants() {
561        assert_ne!(Codepage::ASCII, Codepage::CP037);
562        assert_ne!(Codepage::CP037, Codepage::CP273);
563        assert_ne!(Codepage::CP273, Codepage::CP500);
564        assert_ne!(Codepage::CP500, Codepage::CP1047);
565        assert_ne!(Codepage::CP1047, Codepage::CP1140);
566    }
567
568    #[test]
569    fn test_codepage_debug_format() {
570        let debug = format!("{:?}", Codepage::CP037);
571        assert_eq!(debug, "CP037");
572        let debug = format!("{:?}", Codepage::ASCII);
573        assert_eq!(debug, "ASCII");
574    }
575
576    #[test]
577    fn test_codepage_serde_all_variants_roundtrip() {
578        let variants = [
579            Codepage::ASCII,
580            Codepage::CP037,
581            Codepage::CP273,
582            Codepage::CP500,
583            Codepage::CP1047,
584            Codepage::CP1140,
585        ];
586        for cp in variants {
587            let json = serde_json::to_string(&cp).unwrap();
588            let deserialized: Codepage = serde_json::from_str(&json).unwrap();
589            assert_eq!(cp, deserialized, "Roundtrip failed for {cp}");
590        }
591    }
592
593    #[test]
594    fn test_codepage_from_str_cp037_explicit() {
595        // cp037 should match explicitly, not just as default
596        assert_eq!(
597            <Codepage as std::str::FromStr>::from_str("cp037").unwrap(),
598            Codepage::CP037
599        );
600    }
601
602    #[test]
603    fn test_codepage_display_roundtrip_via_from_str() {
604        let variants = [
605            Codepage::ASCII,
606            Codepage::CP273,
607            Codepage::CP500,
608            Codepage::CP1047,
609            Codepage::CP1140,
610        ];
611        for cp in variants {
612            let displayed = cp.to_string();
613            let parsed: Codepage = displayed.parse().unwrap();
614            assert_eq!(cp, parsed, "Display/FromStr roundtrip failed for {cp}");
615        }
616    }
617
618    #[test]
619    fn test_unmappable_policy_clone_preserves_value() {
620        let policy = UnmappablePolicy::Skip;
621        let cloned = policy;
622        assert_eq!(policy, cloned);
623    }
624
625    #[test]
626    fn test_unmappable_policy_debug_format() {
627        assert_eq!(format!("{:?}", UnmappablePolicy::Error), "Error");
628        assert_eq!(format!("{:?}", UnmappablePolicy::Replace), "Replace");
629        assert_eq!(format!("{:?}", UnmappablePolicy::Skip), "Skip");
630    }
631
632    #[test]
633    fn test_unmappable_policy_serde_all_variants_roundtrip() {
634        let variants = [
635            UnmappablePolicy::Error,
636            UnmappablePolicy::Replace,
637            UnmappablePolicy::Skip,
638        ];
639        for policy in variants {
640            let json = serde_json::to_string(&policy).unwrap();
641            let deserialized: UnmappablePolicy = serde_json::from_str(&json).unwrap();
642            assert_eq!(policy, deserialized, "Roundtrip failed for {policy}");
643        }
644    }
645
646    #[test]
647    fn test_unmappable_policy_eq_different_variants() {
648        assert_ne!(UnmappablePolicy::Error, UnmappablePolicy::Replace);
649        assert_ne!(UnmappablePolicy::Replace, UnmappablePolicy::Skip);
650        assert_ne!(UnmappablePolicy::Skip, UnmappablePolicy::Error);
651    }
652
653    #[test]
654    fn test_unmappable_policy_from_str_empty_defaults_to_error() {
655        assert_eq!(
656            <UnmappablePolicy as std::str::FromStr>::from_str("").unwrap(),
657            UnmappablePolicy::Error
658        );
659    }
660
661    #[test]
662    fn test_space_byte_consistency_with_is_ebcdic() {
663        let variants = [
664            Codepage::ASCII,
665            Codepage::CP037,
666            Codepage::CP273,
667            Codepage::CP500,
668            Codepage::CP1047,
669            Codepage::CP1140,
670        ];
671        for cp in variants {
672            if cp.is_ebcdic() {
673                assert_eq!(space_byte(cp), 0x40, "EBCDIC {cp} should have space 0x40");
674            } else {
675                assert_eq!(space_byte(cp), 0x20, "ASCII should have space 0x20");
676            }
677        }
678    }
679
680    #[test]
681    fn test_codepage_is_ascii_and_is_ebcdic_mutually_exclusive() {
682        let variants = [
683            Codepage::ASCII,
684            Codepage::CP037,
685            Codepage::CP273,
686            Codepage::CP500,
687            Codepage::CP1047,
688            Codepage::CP1140,
689        ];
690        for cp in variants {
691            assert_ne!(
692                cp.is_ascii(),
693                cp.is_ebcdic(),
694                "is_ascii and is_ebcdic must be mutually exclusive for {cp}"
695            );
696        }
697    }
698
699    #[test]
700    fn test_get_zoned_sign_table_ebcdic_positive_nibble_f() {
701        let table = get_zoned_sign_table(Codepage::CP037);
702        // 0xF_ is unsigned/positive default
703        let (is_signed, is_negative) = table[0xF];
704        assert!(is_signed);
705        assert!(!is_negative);
706    }
707
708    #[test]
709    fn test_get_zoned_sign_table_ebcdic_negative_nibble_d() {
710        let table = get_zoned_sign_table(Codepage::CP037);
711        let (is_signed, is_negative) = table[0xD];
712        assert!(is_signed);
713        assert!(is_negative);
714    }
715}