Skip to main content

anyd/codes/pharmacode/
mod.rs

1//! Pharmacode (Laetus) — one-track and two-track.
2//!
3//! # One-track ([`Symbology::Pharmacode`])
4//!
5//! Encodes a single integer `3..=131070` as a run of thin/thick bars separated by
6//! narrow spaces. Reading right-to-left, bar position `n` contributes `2^n` (thin) or
7//! `2·2^n` (thick); equivalently each value maps to a bijective base-2 numeral with
8//! digits {1, 2}. A thin bar is one module wide, a thick bar is `WIDE` modules.
9//!
10//! # Two-track ([`Symbology::PharmacodeTwoTrack`])
11//!
12//! Two-track Pharmacode is *height-modulated*: it encodes an integer `4..=64570080`
13//! as a bijective base-3 numeral whose digits {1, 2, 3} are drawn as a bottom-half,
14//! top-half or full bar. A single-row [`LinearPattern`] (one bit per module) cannot
15//! carry three bar heights, so this crate renders each ternary digit *d* as a bar of
16//! *d* modules separated by single-module spaces — a lossless linearization, not a
17//! directly scannable two-track image. This representation choice is documented here
18//! and reflected in [`Symbol::symbology`].
19//!
20//! Neither variant carries a check digit; the value is self-describing, so
21//! [`PharmacodeMeta`] holds no state.
22//!
23//! # Validation
24//!
25//! One-track is hand-verified against the documented bounds: `3` → two thin bars and
26//! `131070` → sixteen thick bars (Wikipedia). Two-track is hand-verified against its
27//! bounds: `4` → `[1,1]` and `64570080` → sixteen `3`s.
28
29use crate::error::{Error, Result};
30use crate::output::{Encoding, LinearPattern};
31use crate::segment::Segment;
32use crate::symbol::{Symbol, SymbolMeta};
33use crate::symbology::Symbology;
34use crate::traits::{Decode, Encode};
35
36/// Module width of a thick (one-track) bar.
37const WIDE: u32 = 3;
38/// Quiet-zone width in narrow modules on each side.
39const QUIET_ZONE: usize = 10;
40
41/// Inclusive value range of one-track Pharmacode.
42const ONE_TRACK_RANGE: std::ops::RangeInclusive<u32> = 3..=131070;
43/// Inclusive value range of two-track Pharmacode.
44const TWO_TRACK_RANGE: std::ops::RangeInclusive<u32> = 4..=64570080;
45
46/// Parameters required to re-encode a Pharmacode symbol identically. The value lives
47/// in the numeric segment and the track count in [`Symbol::symbology`]; no extra state
48/// is needed.
49#[derive(Debug, Clone, PartialEq, Eq, Default)]
50pub struct PharmacodeMeta;
51
52/// Parse the numeric payload as a `u32`.
53fn parse_value(payload: &[u8]) -> Result<u32> {
54    let text = std::str::from_utf8(payload)
55        .map_err(|_| Error::invalid_data("Pharmacode payload not UTF-8"))?;
56    text.parse::<u32>()
57        .map_err(|_| Error::invalid_data("Pharmacode payload is not an integer"))
58}
59
60/// Append `width` copies of `bar` to `modules`.
61fn push_run(modules: &mut Vec<bool>, bar: bool, width: u32) {
62    modules.extend(std::iter::repeat_n(bar, width as usize));
63}
64
65/// Render a left-to-right sequence of bar widths, joined by single-module spaces.
66fn render_bars(bar_widths: &[u32]) -> Vec<bool> {
67    let mut modules = Vec::new();
68    for (i, &w) in bar_widths.iter().enumerate() {
69        if i > 0 {
70            push_run(&mut modules, false, 1);
71        }
72        push_run(&mut modules, true, w);
73    }
74    modules
75}
76
77/// Run-length encode into element widths, and return only the bar widths (the
78/// spaces, at odd indices, must all be narrow).
79fn bar_widths(modules: &[bool]) -> Result<Vec<u32>> {
80    if modules.is_empty() || !modules[0] {
81        return Err(Error::undecodable("linear pattern must start with a bar"));
82    }
83    let mut runs = Vec::new();
84    let mut cur = modules[0];
85    let mut len = 0u32;
86    for &m in modules {
87        if m == cur {
88            len += 1;
89        } else {
90            runs.push(len);
91            cur = m;
92            len = 1;
93        }
94    }
95    runs.push(len);
96    // Bars at even indices, spaces (all narrow) at odd indices.
97    if runs.len() % 2 == 0 {
98        return Err(Error::undecodable("Pharmacode must end with a bar"));
99    }
100    for (i, &w) in runs.iter().enumerate() {
101        if i % 2 == 1 && w != 1 {
102            return Err(Error::undecodable("Pharmacode space not narrow"));
103        }
104    }
105    Ok(runs.iter().step_by(2).copied().collect())
106}
107
108// ---------- one-track ----------
109
110/// Left-to-right bar widths for a one-track value (thin = 1, thick = `WIDE`).
111fn one_track_bars(mut n: u32) -> Vec<u32> {
112    let mut bars = Vec::new(); // least-significant first
113    while n > 0 {
114        if n % 2 == 1 {
115            bars.push(1); // thin, value 1
116            n = (n - 1) / 2;
117        } else {
118            bars.push(WIDE); // thick, value 2
119            n = (n - 2) / 2;
120        }
121    }
122    bars.reverse(); // most-significant (leftmost) first
123    bars
124}
125
126/// Recover a one-track value from its left-to-right bar widths.
127fn one_track_value(bars: &[u32]) -> Result<u32> {
128    let mut value: u32 = 0;
129    for &w in bars {
130        let digit = match w {
131            1 => 1,
132            _ if w > 1 => 2,
133            _ => return Err(Error::undecodable("invalid Pharmacode bar")),
134        };
135        value = value
136            .checked_mul(2)
137            .and_then(|v| v.checked_add(digit))
138            .ok_or_else(|| Error::undecodable("Pharmacode value overflow"))?;
139    }
140    Ok(value)
141}
142
143// ---------- two-track ----------
144
145/// Left-to-right ternary digits (bar widths 1/2/3) for a two-track value.
146fn two_track_bars(mut n: u32) -> Vec<u32> {
147    let mut digits = Vec::new();
148    while n > 0 {
149        let r = n % 3;
150        if r == 0 {
151            digits.push(3);
152            n = n / 3 - 1;
153        } else {
154            digits.push(r);
155            n /= 3;
156        }
157    }
158    digits.reverse();
159    digits
160}
161
162/// Recover a two-track value from its left-to-right bar widths (1/2/3).
163fn two_track_value(bars: &[u32]) -> Result<u32> {
164    let mut value: u32 = 0;
165    for &w in bars {
166        if !(1..=3).contains(&w) {
167            return Err(Error::undecodable("invalid two-track bar width"));
168        }
169        value = value
170            .checked_mul(3)
171            .and_then(|v| v.checked_add(w))
172            .ok_or_else(|| Error::undecodable("two-track value overflow"))?;
173    }
174    Ok(value)
175}
176
177/// Pharmacode encoder.
178#[derive(Debug, Default, Clone, Copy)]
179pub struct PharmacodeEncoder;
180
181impl PharmacodeEncoder {
182    /// A new encoder.
183    pub fn new() -> Self {
184        Self
185    }
186
187    /// Build a one-track symbol for `value` (`3..=131070`).
188    pub fn build(&self, value: u32) -> Result<Symbol> {
189        if !ONE_TRACK_RANGE.contains(&value) {
190            return Err(Error::capacity(
191                "one-track Pharmacode value out of range 3..=131070",
192            ));
193        }
194        Ok(Symbol::new(
195            Symbology::Pharmacode,
196            vec![Segment::numeric(value.to_string().into_bytes())],
197            SymbolMeta::Pharmacode(PharmacodeMeta),
198        ))
199    }
200
201    /// Build a two-track symbol for `value` (`4..=64570080`).
202    pub fn build_two_track(&self, value: u32) -> Result<Symbol> {
203        if !TWO_TRACK_RANGE.contains(&value) {
204            return Err(Error::capacity(
205                "two-track Pharmacode value out of range 4..=64570080",
206            ));
207        }
208        Ok(Symbol::new(
209            Symbology::PharmacodeTwoTrack,
210            vec![Segment::numeric(value.to_string().into_bytes())],
211            SymbolMeta::Pharmacode(PharmacodeMeta),
212        ))
213    }
214}
215
216impl Encode for PharmacodeEncoder {
217    fn encode(&self, symbol: &Symbol) -> Result<Encoding> {
218        let value = parse_value(&symbol.payload_bytes())?;
219        let bars = match symbol.symbology {
220            Symbology::Pharmacode => {
221                if !ONE_TRACK_RANGE.contains(&value) {
222                    return Err(Error::capacity("one-track Pharmacode value out of range"));
223                }
224                one_track_bars(value)
225            }
226            Symbology::PharmacodeTwoTrack => {
227                if !TWO_TRACK_RANGE.contains(&value) {
228                    return Err(Error::capacity("two-track Pharmacode value out of range"));
229                }
230                two_track_bars(value)
231            }
232            _ => return Err(Error::invalid_parameter("not a Pharmacode symbology")),
233        };
234        if !matches!(symbol.meta, SymbolMeta::Pharmacode(_)) {
235            return Err(Error::invalid_parameter(
236                "Pharmacode symbol missing PharmacodeMeta",
237            ));
238        }
239        Ok(Encoding::Linear(LinearPattern {
240            modules: render_bars(&bars),
241            quiet_zone: QUIET_ZONE,
242        }))
243    }
244}
245
246/// Pharmacode decoder.
247#[derive(Debug, Default, Clone, Copy)]
248pub struct PharmacodeDecoder {
249    two_track: bool,
250}
251
252impl PharmacodeDecoder {
253    /// A new one-track decoder.
254    pub fn new() -> Self {
255        Self { two_track: false }
256    }
257
258    /// A two-track decoder (interprets bar widths 1/2/3 as ternary digits).
259    pub fn two_track() -> Self {
260        Self { two_track: true }
261    }
262}
263
264impl Decode for PharmacodeDecoder {
265    fn decode(&self, encoding: &Encoding) -> Result<Symbol> {
266        let pattern = match encoding {
267            Encoding::Linear(p) => p,
268            Encoding::Matrix(_) => {
269                return Err(Error::invalid_parameter(
270                    "Pharmacode expects a linear pattern",
271                ));
272            }
273        };
274        let bars = bar_widths(&pattern.modules)?;
275        let (value, symbology, range) = if self.two_track {
276            (
277                two_track_value(&bars)?,
278                Symbology::PharmacodeTwoTrack,
279                &TWO_TRACK_RANGE,
280            )
281        } else {
282            (
283                one_track_value(&bars)?,
284                Symbology::Pharmacode,
285                &ONE_TRACK_RANGE,
286            )
287        };
288        if !range.contains(&value) {
289            return Err(Error::undecodable("Pharmacode value out of range"));
290        }
291        Ok(Symbol::new(
292            symbology,
293            vec![Segment::numeric(value.to_string().into_bytes())],
294            SymbolMeta::Pharmacode(PharmacodeMeta),
295        ))
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn one_track_bounds() {
305        // Documented bounds: 3 = two thin bars, 131070 = sixteen thick bars.
306        assert_eq!(one_track_bars(3), vec![1, 1]);
307        assert_eq!(one_track_bars(131070), vec![WIDE; 16]);
308    }
309
310    #[test]
311    fn two_track_bounds() {
312        assert_eq!(two_track_bars(4), vec![1, 1]);
313        assert_eq!(two_track_bars(64570080), vec![3; 16]);
314    }
315
316    #[test]
317    fn one_track_roundtrip() {
318        let enc = PharmacodeEncoder::new();
319        for value in [3u32, 4, 42, 1234, 65535, 131070] {
320            let sym = enc.build(value).unwrap();
321            let encoding = enc.encode(&sym).unwrap();
322            let back = PharmacodeDecoder::new().decode(&encoding).unwrap();
323            assert_eq!(back.segments, sym.segments);
324            assert_eq!(enc.encode(&back).unwrap(), encoding);
325        }
326    }
327
328    #[test]
329    fn two_track_roundtrip() {
330        let enc = PharmacodeEncoder::new();
331        for value in [4u32, 5, 100, 117480, 64570080] {
332            let sym = enc.build_two_track(value).unwrap();
333            let encoding = enc.encode(&sym).unwrap();
334            let back = PharmacodeDecoder::two_track().decode(&encoding).unwrap();
335            assert_eq!(back.symbology, Symbology::PharmacodeTwoTrack);
336            assert_eq!(back.segments, sym.segments);
337            assert_eq!(enc.encode(&back).unwrap(), encoding);
338        }
339    }
340
341    #[test]
342    fn range_checks() {
343        let enc = PharmacodeEncoder::new();
344        assert!(enc.build(2).is_err());
345        assert!(enc.build(131071).is_err());
346        assert!(enc.build_two_track(3).is_err());
347        assert!(enc.build_two_track(64570081).is_err());
348    }
349}