#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Numeric,
Alphanumeric,
Byte,
Kanji,
Eci(u32),
}
impl Mode {
pub fn is_data(&self) -> bool {
!matches!(self, Mode::Eci(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment {
pub mode: Mode,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ModeCost {
pub mode: Mode,
pub head_bits: u32,
pub tail_bits: u32,
pub char_cost_sixths: u32,
pub accepts: fn(u8) -> bool,
}
pub fn optimize_segments(data: &[u8], costs: &[ModeCost]) -> Option<Vec<Segment>> {
const INF: u64 = u64::MAX / 2;
let round_up_bits = |c: u64| c.div_ceil(6) * 6;
if data.is_empty() {
return Some(Vec::new());
}
let mut dp: Vec<u64> = costs
.iter()
.map(|c| {
if (c.accepts)(data[0]) {
u64::from(c.head_bits) * 6 + u64::from(c.char_cost_sixths)
} else {
INF
}
})
.collect();
let mut choice = vec![vec![0u8; costs.len()]; data.len()];
for (i, &b) in data.iter().enumerate().skip(1) {
let mut next = vec![INF; costs.len()];
for (j, cj) in costs.iter().enumerate() {
if !(cj.accepts)(b) {
continue;
}
let mut best = dp[j];
let mut pick = j;
for (k, ck) in costs.iter().enumerate() {
if k == j || dp[k] >= INF {
continue;
}
let switched = round_up_bits(dp[k] + u64::from(ck.tail_bits) * 6)
+ u64::from(cj.head_bits) * 6;
if switched < best {
best = switched;
pick = k;
}
}
if best >= INF {
continue;
}
next[j] = best + u64::from(cj.char_cost_sixths);
choice[i][j] = pick as u8;
}
dp = next;
}
let mut mode_idx = (0..costs.len())
.min_by_key(|&j| dp[j].saturating_add(u64::from(costs[j].tail_bits) * 6))?;
if dp[mode_idx] >= INF {
return None;
}
let mut tags = vec![0u8; data.len()];
for i in (0..data.len()).rev() {
tags[i] = mode_idx as u8;
mode_idx = choice[i][mode_idx] as usize;
}
let mut out = Vec::new();
let mut start = 0;
for i in 1..=data.len() {
if i == data.len() || tags[i] != tags[start] {
out.push(Segment {
mode: costs[tags[start] as usize].mode.clone(),
data: data[start..i].to_vec(),
});
start = i;
}
}
Some(out)
}
impl Segment {
pub fn numeric(digits: impl Into<Vec<u8>>) -> Self {
Segment {
mode: Mode::Numeric,
data: digits.into(),
}
}
pub fn alphanumeric(data: impl Into<Vec<u8>>) -> Self {
Segment {
mode: Mode::Alphanumeric,
data: data.into(),
}
}
pub fn byte(data: impl Into<Vec<u8>>) -> Self {
Segment {
mode: Mode::Byte,
data: data.into(),
}
}
pub fn kanji(data: impl Into<Vec<u8>>) -> Self {
Segment {
mode: Mode::Kanji,
data: data.into(),
}
}
pub fn eci(assignment: u32) -> Self {
Segment {
mode: Mode::Eci(assignment),
data: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_digit(b: u8) -> bool {
b.is_ascii_digit()
}
fn is_alnum(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b' ' | b'$' | b'%' | b'*' | b'+' | b'-' | b'.' | b'/' | b':')
}
fn any(_: u8) -> bool {
true
}
fn qr_costs() -> Vec<ModeCost> {
vec![
ModeCost {
mode: Mode::Numeric,
head_bits: 4 + 10,
tail_bits: 0,
char_cost_sixths: 20,
accepts: is_digit,
},
ModeCost {
mode: Mode::Alphanumeric,
head_bits: 4 + 9,
tail_bits: 0,
char_cost_sixths: 33,
accepts: is_alnum,
},
ModeCost {
mode: Mode::Byte,
head_bits: 4 + 8,
tail_bits: 0,
char_cost_sixths: 48,
accepts: any,
},
]
}
#[test]
fn empty_input_yields_no_segments() {
assert_eq!(optimize_segments(b"", &qr_costs()), Some(Vec::new()));
}
#[test]
fn digits_pick_numeric() {
let segs = optimize_segments(b"0123456789", &qr_costs()).unwrap();
assert_eq!(segs, vec![Segment::numeric(b"0123456789".to_vec())]);
}
#[test]
fn hello_world_picks_alphanumeric() {
let segs = optimize_segments(b"HELLO WORLD", &qr_costs()).unwrap();
assert_eq!(segs, vec![Segment::alphanumeric(b"HELLO WORLD".to_vec())]);
}
#[test]
fn lowercase_falls_back_to_byte() {
let segs = optimize_segments(b"hello", &qr_costs()).unwrap();
assert_eq!(segs, vec![Segment::byte(b"hello".to_vec())]);
}
#[test]
fn short_digit_run_stays_merged() {
let segs = optimize_segments(b"AB12CD", &qr_costs()).unwrap();
assert_eq!(segs, vec![Segment::alphanumeric(b"AB12CD".to_vec())]);
}
#[test]
fn long_digit_run_splits_out() {
let segs =
optimize_segments(b"ABCDEF00000000000000000000000000ABCDEF", &qr_costs()).unwrap();
assert_eq!(
segs,
vec![
Segment::alphanumeric(b"ABCDEF".to_vec()),
Segment::numeric(b"00000000000000000000000000".to_vec()),
Segment::alphanumeric(b"ABCDEF".to_vec()),
]
);
}
#[test]
fn mixed_url_splits_sensibly() {
let segs = optimize_segments(
b"https://example.com/A0123456789012345678901234",
&qr_costs(),
)
.unwrap();
assert_eq!(segs.first().unwrap().mode, Mode::Byte);
assert_eq!(segs.last().unwrap().mode, Mode::Numeric);
let flat: Vec<u8> = segs.iter().flat_map(|s| s.data.clone()).collect();
assert_eq!(
flat,
b"https://example.com/A0123456789012345678901234".to_vec()
);
}
#[test]
fn unrepresentable_byte_returns_none() {
let costs: Vec<ModeCost> = qr_costs().into_iter().take(2).collect();
assert_eq!(optimize_segments(b"abc", &costs), None);
let segs = optimize_segments(b"123", &costs).unwrap();
assert_eq!(segs, vec![Segment::numeric(b"123".to_vec())]);
}
#[test]
fn multibyte_utf8_never_splits_mid_character() {
let text = "abc日本語123456789012345".as_bytes();
let segs = optimize_segments(text, &qr_costs()).unwrap();
let flat: Vec<u8> = segs.iter().flat_map(|s| s.data.clone()).collect();
assert_eq!(flat, text.to_vec());
for s in &segs {
if s.mode == Mode::Byte {
assert!(
std::str::from_utf8(&s.data).is_ok(),
"byte segment split a UTF-8 char"
);
}
}
}
}