//! A sheet's row and column geometry from `.uno:SheetGeometryData`. use std::io; mod analysis; pub mod csv; mod data; mod page; pub use analysis::*; pub use data::*; pub use page::*; /// The query for sizes and hidden flags of both axes. pub const GEOMETRY: &str = ".uno:SheetGeometryData?columns=true&rows=true&sizes=true&hidden=true&filtered=false&groups=false"; /// One axis: runs of equal size, to each last index. #[derive(Clone, Debug, Default, PartialEq)] pub struct Axis { /// (last index, size in twips); hidden runs are 0. runs: Vec<(u32, i32)>, } /// Both axes of the shown sheet. #[derive(Clone, Debug, Default, PartialEq)] pub struct Geometry { pub rows: Axis, pub columns: Axis, } impl Axis { /// How many rows or columns. pub fn count(&self) -> u32 { self.runs.last().map_or(0, |&(last, _)| last + 1) } /// The size of `index` in twips; 0 when hidden. pub fn size(&self, index: u32) -> i32 { self.runs .iter() .find(|&&(last, _)| index <= last) .map_or(0, |&(_, size)| size) } /// Where `index` starts, in twips. pub fn start(&self, index: u32) -> i32 { let (mut at, mut first) = (0i64, 0u32); for &(last, size) in &self.runs { if index <= last { return clamp(at + i64::from(index - first) * i64::from(size)); } at += i64::from(last - first + 1) * i64::from(size); first = last + 1; } clamp(at) } /// The index under `twips`; the last one past the end. pub fn at(&self, twips: i32) -> u32 { let (mut at, mut first) = (0i64, 0u32); let twips = i64::from(twips.max(0)); for &(last, size) in &self.runs { let span = i64::from(last - first + 1) * i64::from(size); if size > 0 && twips < at + span { let into = u32::try_from((twips - at) / i64::from(size)).expect("inside a run"); return first + into; } at += span; first = last + 1; } self.count().saturating_sub(1) } } impl Axis { /// Where `index` starts in tile pixels. fn tile_start(&self, index: u32, ppt: f64) -> i64 { let (mut at, mut first) = (0i64, 0u32); for &(last, size) in &self.runs { let pixels = i64::from(to_pixels(size, ppt)); if index <= last { return at + i64::from(index - first) * pixels; } at += i64::from(last - first + 1) * pixels; first = last + 1; } at } /// A print-twips position in tile twips at `ppt`. pub fn tile(&self, print: i32, ppt: f64) -> i32 { let end = self.start(self.count()); if print >= end { return clamp(twips_of(self.tile_start(self.count(), ppt), ppt) + i64::from(print - end)); } let index = self.at(print); let (from, size) = (self.start(index), self.size(index)); let start = twips_of(self.tile_start(index, ppt), ppt); let span = twips_of(self.tile_start(index + 1, ppt), ppt) - start; clamp(start + i64::from(print - from) * span / i64::from(size)) } /// The print-twips position under tile twips `tile`. pub fn print(&self, tile: i32, ppt: f64) -> i32 { let pixel = (f64::from(tile.max(0)) * ppt) as i64; let (mut at, mut first) = (0i64, 0u32); for &(last, size) in &self.runs { let pixels = i64::from(to_pixels(size, ppt)); let span = i64::from(last - first + 1) * pixels; if pixels > 0 && pixel < at + span { let index = first + u32::try_from((pixel - at) / pixels).expect("inside a run"); let start = twips_of(self.tile_start(index, ppt), ppt); let width = twips_of(self.tile_start(index + 1, ppt), ppt) - start; let into = i64::from(tile) - start; return clamp(i64::from(self.start(index)) + into * i64::from(size) / width.max(1)); } at += span; first = last + 1; } let end = twips_of(self.tile_start(self.count(), ppt), ppt); clamp(i64::from(self.start(self.count())) + i64::from(tile) - end) } } /// The kernel's `ScViewData::ToPixel`: truncates, never to 0. fn to_pixels(twips: i32, ppt: f64) -> i32 { let pixels = (f64::from(twips) * ppt) as i32; if pixels == 0 && twips != 0 { 1 } else { pixels } } fn twips_of(pixels: i64, ppt: f64) -> i64 { (pixels as f64 / ppt).round() as i64 } fn clamp(twips: i64) -> i32 { i32::try_from(twips).unwrap_or(i32::MAX) } /// Parses the kernel's geometry JSON. pub fn geometry(json: &str) -> io::Result { let value: serde_json::Value = serde_json::from_str(json) .map_err(|error| io::Error::other(format!("bad sheet geometry: {error}")))?; let axis = |name: &str| -> io::Result { let field = |key: &str| { value[name][key] .as_str() .ok_or_else(|| io::Error::other(format!("no {name} {key} in the geometry"))) }; merge(&runs(field("sizes")?)?, &flags(field("hidden")?)?) }; Ok(Geometry { rows: axis("rows")?, columns: axis("columns")?, }) } /// "value:last value:last " as (last, value) runs. fn runs(text: &str) -> io::Result> { text.split_whitespace() .map(|run| { let bad = || io::Error::other(format!("a bad geometry run: {run}")); let (value, last) = run.split_once(':').ok_or_else(bad)?; Ok(( last.parse().map_err(|_| bad())?, value.parse().map_err(|_| bad())?, )) }) .collect() } /// "value:last last last ": each later run flips the value. fn flags(text: &str) -> io::Result> { let mut parts = text.split_whitespace(); let bad = |part: &str| io::Error::other(format!("a bad flag run: {part}")); let head = parts.next().ok_or_else(|| bad(text))?; let (value, last) = head.split_once(':').ok_or_else(|| bad(head))?; let mut value: i32 = value.parse().map_err(|_| bad(head))?; let mut list = vec![(last.parse().map_err(|_| bad(head))?, value)]; for part in parts { value = 1 - value; list.push((part.parse().map_err(|_| bad(part))?, value)); } Ok(list) } /// Sizes with hidden runs set to 0. fn merge(sizes: &[(u32, i32)], hidden: &[(u32, i32)]) -> io::Result { let mut runs: Vec<(u32, i32)> = Vec::new(); let (mut size, mut hide) = (sizes.iter().peekable(), hidden.iter().peekable()); let mut first = 0u32; while let (Some(&&(size_last, width)), Some(&&(hide_last, hidden))) = (size.peek(), hide.peek()) { let last = size_last.min(hide_last); let value = if hidden != 0 { 0 } else { width }; match runs.last_mut() { Some((end, run)) if *run == value => *end = last, _ => runs.push((last, value)), } first = last + 1; if size_last == last { size.next(); } if hide_last == last { hide.next(); } } if size.peek().is_some() || hide.peek().is_some() { return Err(io::Error::other(format!( "geometry runs disagree past index {first}" ))); } Ok(Axis { runs }) } /// What refers to a sheet from elsewhere. #[derive(Clone, Debug, Default, PartialEq)] pub struct References { /// Formula cells on other sheets. pub formulas: u64, /// Named ranges. pub names: u64, /// The first such cell, as "$U.A1"; empty when none. pub first: String, /// Formulas and names whose targets show only at run time. pub dynamic: u64, } /// The query for references to sheet `sheet`, from 0. pub fn references_query(sheet: usize) -> String { format!(".uno:SheetReferences?sheet={sheet}") } /// Parses `.uno:SheetReferences`. pub fn references(json: &str) -> io::Result { let value: serde_json::Value = serde_json::from_str(json) .map_err(|error| io::Error::other(format!("bad sheet references: {error}")))?; let number = |key: &str| { value[key] .as_u64() .ok_or_else(|| io::Error::other(format!("no {key} in {value}"))) }; Ok(References { formulas: number("formulas")?, names: number("names")?, first: value["first"] .as_str() .ok_or_else(|| io::Error::other(format!("no first in {value}")))? .to_string(), dynamic: number("dynamic")?, }) } /// The workbook's named ranges (TABLE-06). pub const NAMES: &str = ".uno:DefinedNames"; /// One named range. #[derive(Clone, Debug, PartialEq)] pub struct Name { pub name: String, /// The sheet it belongs to; empty for the workbook. pub scope: String, /// What it refers to, as a formula shows it. pub formula: String, /// The absolute range it names; empty for a formula. pub range: String, /// It lost a reference or holds an error. pub broken: bool, } /// Parses `.uno:DefinedNames`. pub fn names(json: &str) -> io::Result> { let value: serde_json::Value = serde_json::from_str(json) .map_err(|error| io::Error::other(format!("bad names: {error}")))?; let bad = || io::Error::other(format!("unreadable names: {value}")); value["names"] .as_array() .ok_or_else(bad)? .iter() .map(|entry| { let text = |key: &str| entry[key].as_str().map(str::to_string).ok_or_else(bad); Ok(Name { name: text("name")?, scope: text("scope")?, formula: text("formula")?, range: text("range")?, broken: entry["broken"].as_bool().ok_or_else(bad)?, }) }) .collect() } /// The selection as one absolute range (TABLE-06). pub const SELECTED: &str = ".uno:SelectedRange"; /// The selection as one range, and the data inside it. #[derive(Clone, Debug, PartialEq)] pub struct Selection { /// Absolute, with the sheet. pub range: String, /// First column, row, last column, row; None with no data. pub area: Option<(u32, u32, u32, u32)>, /// The area as an absolute range; empty with no data. pub data: String, /// The area's first row. pub top: Vec, /// The kernel takes the first row for headers. pub header: bool, } /// Parses `.uno:SelectedRange`; None when several ranges are selected. pub fn selected_range(json: &str) -> io::Result> { let value: serde_json::Value = serde_json::from_str(json) .map_err(|error| io::Error::other(format!("bad selection: {error}")))?; let bad = || io::Error::other(format!("unreadable selection: {value}")); let range = value["range"].as_str().ok_or_else(bad)?; if range.is_empty() { return Ok(None); } let Some(area) = value["area"].as_array() else { return Ok(Some(Selection { range: range.to_string(), area: None, data: String::new(), top: Vec::new(), header: false, })); }; let corner = |at: usize| -> io::Result { area.get(at) .and_then(|n| n.as_str()?.parse().ok()) .ok_or_else(bad) }; let top = value["top"] .as_array() .ok_or_else(bad)? .iter() .map(|text| text.as_str().map(str::to_string).ok_or_else(bad)) .collect::>()?; Ok(Some(Selection { range: range.to_string(), area: Some((corner(0)?, corner(1)?, corner(2)?, corner(3)?)), data: value["data"].as_str().ok_or_else(bad)?.to_string(), top, header: value["header"].as_bool().ok_or_else(bad)?, })) } /// The query that checks whether `name` can name a range. pub fn name_check_query(name: &str) -> String { format!("{NAMES}?name={}", encode(name)) } /// `text` percent-encoded for a query value. fn encode(text: &str) -> String { text.bytes() .map(|byte| match byte { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { (byte as char).to_string() } other => format!("%{other:02X}"), }) .collect() } /// A name check: "valid", "address" or "characters". pub fn name_check(json: &str) -> io::Result<&'static str> { let value: serde_json::Value = serde_json::from_str(json) .map_err(|error| io::Error::other(format!("bad name check: {error}")))?; match value["name"].as_str() { Some("valid") => Ok("valid"), Some("address") => Ok("address"), Some("characters") => Ok("characters"), _ => Err(io::Error::other(format!("unreadable name check: {value}"))), } } /// The column's letters: 0 is "A", 26 is "AA". pub fn column_name(index: u32) -> String { let mut name = Vec::new(); let mut n = index + 1; while n > 0 { let digit = u8::try_from((n - 1) % 26).expect("a letter"); name.push(b'A' + digit); n = (n - 1) / 26; } name.reverse(); String::from_utf8(name).expect("ASCII letters") } /// The A1 address of a cell, both from 0. pub fn address(column: u32, row: u32) -> String { format!("{}{}", column_name(column), row + 1) } #[cfg(test)] mod tests { use super::*; const JSON: &str = r#"{"commandName":".uno:SheetGeometryData","maxtiledcolumn":"1023","maxtiledrow":"500000", "columns":{"sizes":"1280:1 2000:2 1280:1023 ","hidden":"0:0 1 1023 "}, "rows":{"sizes":"256:1048575 ","hidden":"0:1048575 "}}"#; #[test] fn hidden_columns_fold_and_positions_add_up() { let geometry = geometry(JSON).expect("the geometry parses"); let columns = &geometry.columns; assert_eq!(columns.count(), 1024); assert_eq!(columns.size(1), 0, "B hides"); assert_eq!(columns.size(2), 2000); assert_eq!(columns.start(2), 1280); assert_eq!(columns.start(3), 3280); assert_eq!(columns.at(1279), 0); assert_eq!(columns.at(1280), 2, "a hidden column takes no room"); assert_eq!(columns.at(i32::MAX), 1023); assert_eq!(geometry.rows.start(10), 2560); assert_eq!(geometry.rows.at(2559), 9); } #[test] fn names_and_addresses() { assert_eq!(column_name(0), "A"); assert_eq!(column_name(25), "Z"); assert_eq!(column_name(26), "AA"); assert_eq!(column_name(16383), "XFD"); assert_eq!(address(1, 9), "B10"); } #[test] fn broken_runs_fail() { assert!(runs("12:x").is_err()); assert!(flags("0:3 x").is_err()); assert_eq!(flags("1:2 5").expect("flags"), [(2, 1), (5, 0)]); assert!(merge(&[(3, 10)], &[(5, 0)]).is_err(), "lengths must agree"); } #[test] fn tiles_round_each_row_to_whole_pixels() { // 293 twips at 2/15 px per twip truncate to 39. let rows = Axis { runs: vec![(9999, 293)] }; let ppt = 2.0 / 15.0; assert_eq!(to_pixels(293, ppt), 39); let row = 4997u32; let print = rows.start(row); let tile = rows.tile(print, ppt); assert_eq!(tile, (f64::from(39 * row) / ppt).round() as i32, "row tops line up"); assert_eq!(rows.print(tile, ppt), print, "and come back"); let mid = rows.tile(print + 146, ppt); assert_eq!(rows.at(rows.print(mid, ppt)), row, "a point inside stays in its row"); assert!(tile < print, "rounding down pulls far rows up"); } #[test] fn hidden_rows_take_no_tile_space_and_tiny_rows_take_a_pixel() { let rows = Axis { runs: vec![(1, 300), (3, 0), (4, 3)] }; let ppt = 1.0 / 15.0; assert_eq!(rows.tile(rows.start(4), ppt), rows.tile(rows.start(2), ppt), "hidden rows fold"); assert_eq!(to_pixels(3, ppt), 1, "never 0 for a shown row"); } }