use std::cmp::Ordering;
use gents_derives::TS;
use logisheets_base::{BlockFieldId, BlockId, CellId, CellValue};
use crate::{
block_manager::schema_manager::schema::{Schema, SchemaTrait},
errors::Error,
};
use super::Workbook;
#[derive(Debug, Clone, TS)]
#[ts(file_name = "block_sort_order.ts", rename_all = "camelCase")]
pub struct BlockSortOrder {
pub is_row: bool,
pub new_order: Vec<usize>,
}
#[derive(Debug, Clone)]
enum SortKey {
Number(f64),
Text(String),
Bool(bool),
Error,
Blank,
}
impl SortKey {
fn category(&self) -> u8 {
match self {
SortKey::Number(_) => 0,
SortKey::Text(_) => 1,
SortKey::Bool(_) => 2,
SortKey::Error => 3,
SortKey::Blank => 4,
}
}
fn cmp_non_blank(&self, other: &Self) -> Ordering {
let (a, b) = (self.category(), other.category());
if a != b {
return a.cmp(&b);
}
match (self, other) {
(SortKey::Number(x), SortKey::Number(y)) => x.total_cmp(y),
(SortKey::Text(x), SortKey::Text(y)) => x.cmp(y),
(SortKey::Bool(x), SortKey::Bool(y)) => x.cmp(y),
_ => Ordering::Equal,
}
}
}
fn cmp_keys(a: &SortKey, b: &SortKey, asc: bool) -> Ordering {
match (matches!(a, SortKey::Blank), matches!(b, SortKey::Blank)) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let ord = a.cmp_non_blank(b);
if asc { ord } else { ord.reverse() }
}
}
}
impl Workbook {
pub fn get_block_sort_order(
&self,
sheet_idx: usize,
block_id: BlockId,
field: &str,
asc: bool,
) -> crate::errors::Result<BlockSortOrder> {
let status = self.status();
let sheet_id = status
.sheet_info_manager
.get_sheet_id(sheet_idx)
.ok_or(Error::UnavailableSheetIdx(sheet_idx))?;
let schema = status
.block_schema_manager
.schemas
.get(&(sheet_id, block_id))
.ok_or_else(|| {
Error::PayloadError(format!(
"block {block_id} on sheet {sheet_idx} has no schema to sort by"
))
})?;
let is_row = match schema {
Schema::RowSchema(_) => true,
Schema::ColSchema(_) => false,
Schema::RandomSchema(_) => {
return Err(Error::PayloadError(
"cannot sort a random-schema block: it has no fields".to_string(),
));
}
};
let field_id: BlockFieldId = schema.resolve_field_id(field).ok_or_else(|| {
Error::PayloadError(format!("unknown field \"{field}\" in block {block_id}"))
})?;
let bp = status.navigator.get_block_place(&sheet_id, &block_id)?;
let key_cells = schema.get_all_key_cell_ids(block_id, bp);
let lines: Vec<u32> = if is_row {
bp.rows.iter().copied().collect()
} else {
bp.cols.iter().copied().collect()
};
let line_of = |cell: &logisheets_base::BlockCellId| -> usize {
let id = if is_row { cell.row } else { cell.col };
lines.iter().position(|l| *l == id).unwrap_or(0)
};
let header_line = schema
.header_line()
.and_then(|h| lines.iter().position(|l| *l == h));
let text_fetcher = |id| status.text_id_manager.get_string(&id).unwrap_or_default();
let mut keyed: Vec<(usize, SortKey)> = key_cells
.iter()
.map(|key_cell| {
let idx = line_of(key_cell);
let sort_key = schema
.partially_resolve_by_field_id(*key_cell, field_id)
.and_then(|field_cell| {
status
.container
.get_cell(sheet_id, &CellId::BlockCell(field_cell))
.map(|c| c.value.clone())
})
.map(|value| to_sort_key(value, &text_fetcher))
.unwrap_or(SortKey::Blank);
(idx, sort_key)
})
.collect();
keyed.sort_by(|(_, a), (_, b)| cmp_keys(a, b, asc));
let new_order: Vec<usize> = header_line
.into_iter()
.chain(keyed.into_iter().map(|(idx, _)| idx))
.collect();
Ok(BlockSortOrder { is_row, new_order })
}
}
pub(super) fn cmp_display_strings(a: &str, b: &str) -> Ordering {
let key = |s: &str| match s.parse::<f64>() {
Ok(n) => SortKey::Number(n),
Err(_) => SortKey::Text(s.to_lowercase()),
};
cmp_keys(&key(a), &key(b), true)
}
fn to_sort_key<F>(value: CellValue, text_fetcher: &F) -> SortKey
where
F: Fn(logisheets_base::TextId) -> String,
{
match value {
CellValue::Blank => SortKey::Blank,
CellValue::Number(n) => SortKey::Number(n),
CellValue::Boolean(b) => SortKey::Bool(b),
CellValue::Error(_) => SortKey::Error,
CellValue::String(id) => SortKey::Text(text_fetcher(id).to_lowercase()),
CellValue::FormulaStr(s) => match s.parse::<f64>() {
Ok(n) => SortKey::Number(n),
Err(_) => SortKey::Text(s.to_lowercase()),
},
CellValue::InlineStr(rst) => SortKey::Text(rst.plain_text()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key_num(n: f64) -> SortKey {
SortKey::Number(n)
}
fn key_text(s: &str) -> SortKey {
SortKey::Text(s.to_string())
}
fn sort_indices(mut keys: Vec<SortKey>, asc: bool) -> Vec<usize> {
let mut idx: Vec<usize> = (0..keys.len()).collect();
idx.sort_by(|&a, &b| cmp_keys(&keys[a], &keys[b], asc));
keys.clear();
idx
}
#[test]
fn numbers_sort_numerically_not_lexically() {
let keys = vec![key_num(2.), key_num(10.), key_num(1.)];
assert_eq!(sort_indices(keys, true), vec![2, 0, 1]);
}
#[test]
fn descending_reverses_non_blanks() {
let keys = vec![key_num(2.), key_num(10.), key_num(1.)];
assert_eq!(sort_indices(keys, false), vec![1, 0, 2]);
}
#[test]
fn blanks_sort_last_in_both_directions() {
let keys = vec![SortKey::Blank, key_num(5.), key_num(1.)];
assert_eq!(sort_indices(keys.clone(), true), vec![2, 1, 0]);
assert_eq!(sort_indices(keys, false), vec![1, 2, 0]);
}
#[test]
fn numbers_before_text() {
let keys = vec![key_text("apple"), key_num(3.)];
assert_eq!(sort_indices(keys, true), vec![1, 0]);
}
#[test]
fn stable_for_equal_keys() {
let keys = vec![key_num(1.), key_num(1.), key_num(1.)];
assert_eq!(sort_indices(keys, true), vec![0, 1, 2]);
}
}