use odox_core::doc::{Sheet, SheetDocument};
const DEFAULT_ROW: f32 = 13.0;
const DEFAULT_COLUMN: f32 = 64.0;
const MINIMUM: f32 = 4.0;
const MARGIN_CELLS: usize = 40;
pub struct Metrics {
pub rows: Vec<f32>,
pub columns: Vec<f32>,
}
impl Metrics {
pub fn of(document: &SheetDocument, sheet: &Sheet, zoom: f32) -> Self {
let row_count = sheet.used_rows + MARGIN_CELLS;
let column_count = sheet.used_columns.max(1) + MARGIN_CELLS;
let mut rows = Vec::with_capacity(row_count + 1);
let mut at = 0.0;
for row in 0..row_count {
rows.push(at);
let height = document
.row_height(sheet, row)
.map_or(DEFAULT_ROW, odox_core::Length::points);
at += (height * zoom).max(MINIMUM);
}
rows.push(at);
let mut columns = Vec::with_capacity(column_count + 1);
let mut at = 0.0;
for column in 0..column_count {
columns.push(at);
let width = sheet
.column_width(column)
.map_or(DEFAULT_COLUMN, odox_core::Length::points);
at += (width * zoom).max(MINIMUM);
}
columns.push(at);
Self { rows, columns }
}
pub fn size(&self) -> (f32, f32) {
(
self.columns.last().copied().unwrap_or(0.0),
self.rows.last().copied().unwrap_or(0.0),
)
}
pub fn rows_between(&self, top: f32, bottom: f32) -> std::ops::Range<usize> {
span(&self.rows, top, bottom)
}
pub fn columns_between(&self, left: f32, right: f32) -> std::ops::Range<usize> {
span(&self.columns, left, right)
}
pub fn row(&self, index: usize) -> (f32, f32) {
at(&self.rows, index)
}
pub fn column(&self, index: usize) -> (f32, f32) {
at(&self.columns, index)
}
pub fn row_at(&self, offset: f32) -> usize {
index_at(&self.rows, offset)
}
pub fn column_at(&self, offset: f32) -> usize {
index_at(&self.columns, offset)
}
}
fn at(offsets: &[f32], index: usize) -> (f32, f32) {
let start = offsets.get(index).copied().unwrap_or(0.0);
let end = offsets.get(index + 1).copied().unwrap_or(start);
(start, end - start)
}
fn span(offsets: &[f32], from: f32, to: f32) -> std::ops::Range<usize> {
let first = index_at(offsets, from);
let last = index_at(offsets, to);
first..(last + 1).min(offsets.len().saturating_sub(1))
}
fn index_at(offsets: &[f32], offset: f32) -> usize {
let offset = offset.max(0.0);
offsets
.partition_point(|start| *start <= offset)
.saturating_sub(1)
.min(offsets.len().saturating_sub(2))
}
pub fn column_name(mut column: usize) -> String {
let mut name = Vec::new();
loop {
name.push(b'A' + u8::try_from(column % 26).unwrap_or(0));
if column < 26 {
break;
}
column = column / 26 - 1;
}
name.reverse();
String::from_utf8(name).unwrap_or_default()
}
pub fn address(row: usize, column: usize) -> String {
format!("{}{}", column_name(column), row + 1)
}
#[cfg(test)]
mod tests {
use super::{Metrics, address, column_name, index_at};
fn metrics() -> Metrics {
Metrics {
rows: vec![0.0, 10.0, 25.0, 45.0],
columns: vec![0.0, 20.0, 40.0],
}
}
#[test]
fn size_is_the_last_offset_in_each_table() {
assert_eq!(metrics().size(), (40.0, 45.0));
}
#[test]
fn size_of_an_empty_table_is_zero() {
let empty = Metrics {
rows: Vec::new(),
columns: Vec::new(),
};
assert_eq!(empty.size(), (0.0, 0.0));
}
#[test]
fn row_and_column_give_a_start_and_a_length() {
let metrics = metrics();
assert_eq!(metrics.row(0), (0.0, 10.0));
assert_eq!(metrics.row(2), (25.0, 20.0));
assert_eq!(metrics.column(1), (20.0, 20.0));
assert_eq!(metrics.row(3), (45.0, 0.0));
assert_eq!(metrics.row(50), (0.0, 0.0));
}
#[test]
fn row_at_and_column_at_answer_through_the_same_search_as_index_at() {
let metrics = metrics();
assert_eq!(metrics.row_at(24.9), 1);
assert_eq!(metrics.column_at(20.0), 1);
}
#[test]
fn rows_between_covers_every_row_a_span_touches() {
let metrics = metrics();
assert_eq!(metrics.rows_between(5.0, 30.0), 0..3);
assert_eq!(metrics.rows_between(1.0, 2.0), 0..1);
}
#[test]
fn columns_between_stops_at_the_last_real_column() {
let metrics = metrics();
assert_eq!(metrics.columns_between(0.0, 1000.0), 0..2);
}
#[test]
fn column_names_carry_the_way_a_spreadsheet_does() {
assert_eq!(column_name(0), "A");
assert_eq!(column_name(25), "Z");
assert_eq!(column_name(26), "AA");
assert_eq!(column_name(27), "AB");
assert_eq!(column_name(51), "AZ");
assert_eq!(column_name(52), "BA");
assert_eq!(column_name(701), "ZZ");
assert_eq!(column_name(702), "AAA");
assert_eq!(column_name(16_383), "XFD");
assert_eq!(address(6, 1), "B7");
}
#[test]
fn an_offset_finds_the_cell_it_is_inside() {
let offsets = [0.0, 10.0, 25.0, 45.0];
assert_eq!(index_at(&offsets, 0.0), 0);
assert_eq!(index_at(&offsets, 9.9), 0);
assert_eq!(index_at(&offsets, 10.0), 1);
assert_eq!(index_at(&offsets, 24.9), 1);
assert_eq!(index_at(&offsets, 25.0), 2);
assert_eq!(index_at(&offsets, 1000.0), 2);
assert_eq!(index_at(&offsets, -5.0), 0);
}
}