use logisheets_base::{CellId, NormalRange, Range, SheetId, index_to_column_label};
use crate::id_manager::SheetIdManager;
use crate::navigator::Navigator;
#[derive(Debug, Clone)]
pub struct ChartRange {
pub sheet: SheetId,
pub range: Range,
}
#[derive(Debug, Clone, Default)]
pub struct ChartRefs {
pub cat: Option<ChartRange>,
pub series: Vec<ChartSeriesRefs>,
}
#[derive(Debug, Clone, Default)]
pub struct ChartSeriesRefs {
pub val: Option<ChartRange>,
pub size: Option<ChartRange>,
}
impl ChartRefs {
pub fn from_data(
nav: &Navigator,
sheets: &SheetIdManager,
default_sheet: SheetId,
data: &logisheets_workbook::prelude::ChartData,
) -> Self {
let parse = |r: &Option<String>| {
r.as_deref()
.and_then(|r| ChartRange::parse(nav, sheets, default_sheet, r))
};
ChartRefs {
cat: parse(&data.cat_ref),
series: data
.series
.iter()
.map(|s| ChartSeriesRefs {
val: parse(&s.val_ref),
size: parse(&s.size_ref),
})
.collect(),
}
}
pub fn series_at(&self, i: usize) -> Option<&ChartSeriesRefs> {
self.series.get(i)
}
}
impl ChartRange {
pub fn parse(
nav: &Navigator,
sheets: &SheetIdManager,
default_sheet: SheetId,
text: &str,
) -> Option<Self> {
let (sheet_name, sr, sc, er, ec) = parse_a1_range(text)?;
let sheet = match sheet_name {
Some(name) => *sheets.get_id(&name)?,
None => default_sheet,
};
let start = nav.fetch_cell_id(&sheet, sr, sc).ok()?;
let end = nav.fetch_cell_id(&sheet, er, ec).ok()?;
Some(ChartRange {
sheet,
range: addr_range(start, end)?,
})
}
pub fn to_a1(&self, nav: &Navigator, sheets: &SheetIdManager) -> Option<String> {
let (start, end) = match &self.range {
Range::Normal(NormalRange::AddrRange(s, e)) => {
(CellId::NormalCell(s.clone()), CellId::NormalCell(e.clone()))
}
Range::Normal(NormalRange::Single(s)) => {
(CellId::NormalCell(s.clone()), CellId::NormalCell(s.clone()))
}
Range::Block(logisheets_base::BlockRange::AddrRange(s, e)) => {
(CellId::BlockCell(*s), CellId::BlockCell(*e))
}
Range::Block(logisheets_base::BlockRange::Single(s)) => {
(CellId::BlockCell(*s), CellId::BlockCell(*s))
}
_ => return None,
};
let (sr, sc) = nav.fetch_cell_idx(&self.sheet, &start).ok()?;
let (er, ec) = nav.fetch_cell_idx(&self.sheet, &end).ok()?;
let name = sheets.get_string(&self.sheet)?;
Some(format_a1(
&name,
sr.min(er),
sc.min(ec),
sr.max(er),
sc.max(ec),
))
}
}
fn addr_range(start: CellId, end: CellId) -> Option<Range> {
match (start, end) {
(CellId::NormalCell(s), CellId::NormalCell(e)) => {
Some(Range::Normal(NormalRange::AddrRange(s, e)))
}
(CellId::BlockCell(s), CellId::BlockCell(e)) if s.block_id == e.block_id => {
Some(Range::Block(logisheets_base::BlockRange::AddrRange(s, e)))
}
_ => None,
}
}
pub fn format_a1(sheet: &str, sr: usize, sc: usize, er: usize, ec: usize) -> String {
let needs_quote = sheet.is_empty()
|| sheet
.chars()
.any(|c| !(c.is_alphanumeric() || c == '_' || c == '.'))
|| sheet.chars().next().is_some_and(|c| c.is_ascii_digit());
let name = if needs_quote {
format!("'{}'", sheet.replace('\'', "''"))
} else {
sheet.to_string()
};
format!(
"{}!${}${}:${}${}",
name,
index_to_column_label(sc),
sr + 1,
index_to_column_label(ec),
er + 1,
)
}
pub fn parse_a1_range(s: &str) -> Option<(Option<String>, usize, usize, usize, usize)> {
let (sheet, range) = match s.rfind('!') {
Some(i) => {
let mut name = s[..i].to_string();
if name.len() >= 2 && name.starts_with('\'') && name.ends_with('\'') {
name = name[1..name.len() - 1].replace("''", "'");
}
(Some(name), &s[i + 1..])
}
None => (None, s),
};
let (start, end) = match range.split_once(':') {
Some((a, b)) => (a, b),
None => (range, range),
};
let (c1, r1) = parse_a1_cell(start)?;
let (c2, r2) = parse_a1_cell(end)?;
Some((sheet, r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2)))
}
fn parse_a1_cell(s: &str) -> Option<(usize, usize)> {
let s = s.replace('$', "");
let bytes = s.as_bytes();
let mut i = 0;
let mut col = 0usize;
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
col = col * 26 + ((bytes[i].to_ascii_uppercase() - b'A') as usize + 1);
i += 1;
}
if i == 0 || i >= bytes.len() {
return None;
}
let row: usize = s[i..].parse().ok()?;
if col == 0 || row == 0 {
return None;
}
Some((col - 1, row - 1))
}