Skip to main content

saturnus_objects/
charset.rs

1//! The HP 48/49 character set as Unicode text.
2//!
3//! Bytes 0-127 are ASCII and 160-255 ISO 8859-1; 128-159 are HP's math and
4//! Greek characters (wiki: protocols/hp-object-format, the same table as
5//! satx-core's charset, checked there against a 48SX string holding every
6//! character from 128 to 255).
7
8/// Characters 128-159 in byte order.
9const HIGH: [&str; 32] = [
10    "∡",
11    "x\u{0304}",
12    "∇",
13    "√",
14    "∫",
15    "Σ",
16    "▶",
17    "π",
18    "∂",
19    "≤",
20    "≥",
21    "≠",
22    "α",
23    "→",
24    "←",
25    "↓",
26    "↑",
27    "γ",
28    "δ",
29    "ε",
30    "η",
31    "θ",
32    "λ",
33    "ρ",
34    "σ",
35    "τ",
36    "ω",
37    "Δ",
38    "Π",
39    "Ω",
40    "■",
41    "∞",
42];
43
44/// HP bytes as text; every byte has a meaning, so this never fails.
45pub fn decode(bytes: &[u8]) -> String {
46    let mut s = String::with_capacity(bytes.len());
47    for &b in bytes {
48        match b {
49            0x80..=0x9F => s.push_str(HIGH[usize::from(b - 0x80)]),
50            _ => s.push(char::from(b)),
51        }
52    }
53    s
54}
55
56/// One HP byte as text.
57pub fn char_of(b: u8) -> String {
58    decode(&[b])
59}
60
61/// Text as HP bytes: ASCII and Latin-1 as themselves, the math and Greek
62/// characters of 128-159 from their Unicode forms (`x̄` is `x` with a
63/// combining macron). `Err` names the first character the set lacks
64/// (C1 controls and anything beyond U+00FF that is not one of 128-159).
65pub fn encode(text: &str) -> Result<Vec<u8>, char> {
66    let mut out = Vec::with_capacity(text.len());
67    let mut chars = text.chars().peekable();
68    while let Some(c) = chars.next() {
69        if c == 'x' && chars.peek() == Some(&'\u{0304}') {
70            chars.next();
71            out.push(0x81);
72            continue;
73        }
74        let mut buf = [0u8; 4];
75        let s: &str = c.encode_utf8(&mut buf);
76        if let Some(i) = HIGH.iter().position(|&h| h == s) {
77            out.push(0x80 + i as u8);
78            continue;
79        }
80        match u32::from(c) {
81            n @ (0..=0x7F | 0xA0..=0xFF) => out.push(n as u8),
82            _ => return Err(c),
83        }
84    }
85    Ok(out)
86}
87
88/// The ASCII trigraphs of characters 128-159, in byte order: the
89/// calculator's translation modes 2 and 3 (`TRANSIO`) write them in ASCII
90/// transfers (wiki: protocols/hp-object-format).
91const HIGH_TRIGRAPHS: [&str; 32] = [
92    "\\<)", "\\x-", "\\.V", "\\v/", "\\.S", "\\GS", "\\|>", "\\pi", "\\.d", "\\<=", "\\>=", "\\=/",
93    "\\Ga", "\\->", "\\<-", "\\|v", "\\|^", "\\Gg", "\\Gd", "\\Ge", "\\Gn", "\\Gh", "\\Gl", "\\Gr",
94    "\\Gs", "\\Gt", "\\Gw", "\\GD", "\\PI", "\\GW", "\\[]", "\\oo",
95];
96
97/// Characters 160-255 with a mnemonic trigraph; the others are `\nnn`.
98const LATIN_TRIGRAPHS: [(u8, &str); 8] = [
99    (171, "\\<<"),
100    (176, "\\^o"),
101    (181, "\\Gm"),
102    (187, "\\>>"),
103    (215, "\\.x"),
104    (216, "\\O/"),
105    (223, "\\Gb"),
106    (247, "\\:-"),
107];
108
109/// [`encode`] for text typed in ASCII, such as a Kermit host command:
110/// the trigraphs (`\->`, `\<<`, `\GS`, `\160`) become their bytes, since
111/// the calculator does not read them in a host command itself. A
112/// backslash that starts no trigraph stays a backslash.
113pub fn encode_command(text: &str) -> Result<Vec<u8>, char> {
114    let mut out = Vec::with_capacity(text.len());
115    let mut rest = text;
116    while let Some(pos) = rest.find('\\') {
117        out.extend(encode(&rest[..pos])?);
118        let tail = &rest[pos..];
119        match trigraph_at(tail) {
120            Some((byte, len)) => {
121                out.push(byte);
122                rest = &tail[len..];
123            }
124            None => {
125                out.push(b'\\');
126                rest = &tail[1..];
127            }
128        }
129    }
130    out.extend(encode(rest)?);
131    Ok(out)
132}
133
134/// The byte and length of the trigraph that starts `s` (at a backslash).
135fn trigraph_at(s: &str) -> Option<(u8, usize)> {
136    if let Some(head) = s.get(..3) {
137        if let Some(i) = HIGH_TRIGRAPHS.iter().position(|t| *t == head) {
138            return Some((0x80 + i as u8, 3));
139        }
140        if let Some((b, _)) = LATIN_TRIGRAPHS.iter().find(|(_, t)| *t == head) {
141            return Some((*b, 3));
142        }
143    }
144    let digits = s.get(1..4)?;
145    if !digits.bytes().all(|d| d.is_ascii_digit()) {
146        return None;
147    }
148    let n: u16 = digits.parse().ok()?;
149    u8::try_from(n).ok().filter(|b| *b >= 128).map(|b| (b, 4))
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn trigraphs_become_their_bytes() {
158        assert_eq!(
159            encode_command("\\<< 1 \\->LIST \\>> \\GS \\160 \\q").unwrap(),
160            [
161                0xAB, b' ', b'1', b' ', 0x8D, b'L', b'I', b'S', b'T', b' ', 0xBB, b' ', 0x85, b' ',
162                160, b' ', b'\\', b'q'
163            ]
164        );
165        assert_eq!(encode_command("\\127").unwrap(), b"\\127");
166        assert_eq!(encode_command("€"), Err('€'));
167        for (i, t) in HIGH_TRIGRAPHS.iter().enumerate() {
168            assert_eq!(encode_command(t).unwrap(), [0x80 + i as u8]);
169        }
170    }
171
172    #[test]
173    fn encode_is_decode_inverted() {
174        let all: Vec<u8> = (0..=255).collect();
175        assert_eq!(encode(&decode(&all)).unwrap(), all);
176        assert_eq!(
177            encode("« 1 2 + »").unwrap(),
178            [0xAB, 32, 49, 32, 50, 32, 43, 32, 0xBB]
179        );
180        assert_eq!(encode("\u{85}"), Err('\u{85}'));
181        assert_eq!(encode("€"), Err('€'));
182    }
183
184    #[test]
185    fn high_characters() {
186        assert_eq!(decode(b"AB"), "AB");
187        assert_eq!(decode(&[0x8D, 0x85, 0xAB, 0xBB]), "→Σ«»");
188        assert_eq!(decode(&[0x81]), "x\u{0304}");
189    }
190}