Files
ElyOffice/crates/ely-lok/src/sheet.rs
T

460 lines
16 KiB
Rust

//! 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<Geometry> {
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<Axis> {
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<Vec<(u32, i32)>> {
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<Vec<(u32, i32)>> {
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<Axis> {
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<References> {
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<Vec<Name>> {
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<String>,
/// 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<Option<Selection>> {
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<u32> {
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::<io::Result<_>>()?;
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");
}
}