use crate::error::ParseError;
pub const COUNT_AT: usize = 785;
pub const RECORDS_AT: usize = COUNT_AT + 1;
pub const RECORD_LEN: usize = 15;
const STROKE_ID: usize = 2;
const TOP_NOTE: usize = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Zone {
pub top_note: u8,
pub stroke_id: u8,
}
pub fn count(map: &[u8]) -> Result<usize, ParseError> {
map.get(COUNT_AT).map(|n| *n as usize).ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for a zone table",
map.len()
))
})
}
pub fn read(map: &[u8]) -> Result<Vec<Zone>, ParseError> {
let n = count(map)?;
let need = RECORDS_AT + n * RECORD_LEN;
if map.len() < need {
return Err(ParseError::AssertFail(format!(
"map declares {n} zones, needing {need} bytes, but the section is {}",
map.len()
)));
}
Ok((0..n)
.map(|i| {
let r = &map[RECORDS_AT + i * RECORD_LEN..][..RECORD_LEN];
Zone {
top_note: r[TOP_NOTE],
stroke_id: r[STROKE_ID],
}
})
.collect())
}
pub fn set_top_note(map: &mut [u8], index: usize, note: u8) -> Result<(), ParseError> {
let n = count(map)?;
if index >= n {
return Err(ParseError::AssertFail(format!(
"zone {index} out of range, the instrument has {n}"
)));
}
map[RECORDS_AT + index * RECORD_LEN + TOP_NOTE] = note;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ZoneV3 {
pub stroke_gid: u32,
pub root_key: u8,
pub top_note: u8,
pub low_note: Option<u8>,
}
pub fn read_v3(
map_version: u32,
map: &[u8],
strokes: &[(u32, u8)],
) -> Result<Vec<ZoneV3>, ParseError> {
let (record_len, gid_at, trailer, has_low) = match map_version {
12 => (11usize, 5usize, 0usize, false),
14 => (16, 8, 1, true),
21 => (16, 8, 2, true),
v => {
return Err(ParseError::AssertFail(format!(
"map section version {v} has no zone layout derived from a specimen"
)))
}
};
let n = strokes.len();
let start = map
.len()
.checked_sub(trailer + n * record_len)
.filter(|&s| s >= 1)
.ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for {n} zone records",
map.len()
))
})?;
if map[start - 1] as usize != n {
return Err(ParseError::AssertFail(format!(
"zone count {} does not match the {n} strokes",
map[start - 1]
)));
}
(0..n)
.map(|i| {
let r = &map[start + i * record_len..][..record_len];
let gid = u32::from_be_bytes(r[gid_at..gid_at + 4].try_into().unwrap());
let root = strokes.iter().find(|(g, _)| *g == gid).map(|(_, r)| *r);
match root {
Some(root) if root == r[0] => Ok(ZoneV3 {
stroke_gid: gid,
root_key: r[0],
top_note: r[1],
low_note: has_low.then(|| r[2]),
}),
Some(root) => Err(ParseError::AssertFail(format!(
"zone {i} carries root {} but its stroke {gid} holds {root}",
r[0]
))),
None => Err(ParseError::AssertFail(format!(
"zone {i} references stroke {gid}, which the body does not hold"
))),
}
})
.collect()
}
pub fn derive_top_notes(roots_high_to_low: &[u8]) -> Vec<u8> {
roots_high_to_low
.iter()
.enumerate()
.map(|(i, &root)| {
if i == 0 {
root.saturating_add(24)
} else {
let above = roots_high_to_low[i - 1];
(u16::from(root) + u16::from(above)).div_ceil(2) as u8 - 1
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn table(tops: &[u8]) -> Vec<u8> {
table_with_ids(tops, &(1..=tops.len() as u8).rev().collect::<Vec<_>>())
}
fn table_with_ids(tops: &[u8], ids: &[u8]) -> Vec<u8> {
let mut m = vec![0u8; RECORDS_AT + tops.len() * RECORD_LEN];
m[COUNT_AT] = tops.len() as u8;
for (i, (&t, &id)) in tops.iter().zip(ids).enumerate() {
let r = RECORDS_AT + i * RECORD_LEN;
m[r + STROKE_ID] = id;
m[r + TOP_NOTE] = t;
}
m
}
#[test]
fn reads_the_table() {
let zones = read(&table(&[96, 65, 53])).unwrap();
assert_eq!(zones.len(), 3);
assert_eq!(zones[0].top_note, 96);
assert_eq!(zones[2].top_note, 53);
assert_eq!(zones[0].stroke_id, 3);
assert_eq!(zones[2].stroke_id, 1);
}
#[test]
fn stroke_ids_need_not_be_a_countdown() {
let zones = read(&table_with_ids(
&[108, 90, 77, 66, 60, 53],
&[13, 12, 6, 9, 5, 25],
))
.unwrap();
assert_eq!(
zones.iter().map(|z| z.stroke_id).collect::<Vec<_>>(),
[13, 12, 6, 9, 5, 25]
);
}
#[test]
fn set_top_note_moves_exactly_one_byte() {
let before = table(&[96, 65, 53]);
let mut after = before.clone();
set_top_note(&mut after, 1, 60).unwrap();
let differing: Vec<_> = (0..before.len())
.filter(|&i| before[i] != after[i])
.collect();
assert_eq!(differing, vec![RECORDS_AT + RECORD_LEN + TOP_NOTE]);
assert_eq!(read(&after).unwrap()[1].top_note, 60);
}
#[test]
fn out_of_range_zone_is_rejected() {
let mut m = table(&[96, 65]);
assert!(set_top_note(&mut m, 2, 60).is_err());
}
#[test]
fn short_map_is_rejected() {
assert!(read(&[0u8; 16]).is_err());
let mut m = table(&[96, 65]);
m[COUNT_AT] = 9; assert!(read(&m).is_err());
}
#[test]
fn derived_ranges_match_the_editor() {
assert_eq!(derive_top_notes(&[72, 60, 48]), vec![96, 65, 53]);
assert_eq!(derive_top_notes(&[60, 48]), vec![84, 53]);
assert_eq!(derive_top_notes(&[60]), vec![84]);
}
#[test]
fn derived_ranges_handle_an_odd_gap() {
assert_eq!(derive_top_notes(&[61, 60]), vec![85, 60]);
}
}