use std::collections::HashMap;
use crate::ir::{Color, DataBarInfo};
use crate::parser::xlsx::{CellPos, CellRange, parse_cell_ref};
use crate::parser::xml_util;
#[derive(Default)]
pub(crate) struct CondFmtOverride {
pub background: Option<Color>,
pub font_color: Option<Color>,
pub bold: Option<bool>,
pub data_bar: Option<DataBarInfo>,
pub icon_text: Option<String>,
}
fn parse_sqref(sqref: &str) -> Vec<CellRange> {
sqref
.split_whitespace()
.filter_map(|part| {
if let Some((start_str, end_str)) = part.split_once(':') {
let (sc, sr) = parse_cell_ref(start_str)?;
let (ec, er) = parse_cell_ref(end_str)?;
Some(CellRange {
start_col: sc,
start_row: sr,
end_col: ec,
end_row: er,
})
} else {
let (c, r) = parse_cell_ref(part)?;
Some(CellRange {
start_col: c,
start_row: r,
end_col: c,
end_row: r,
})
}
})
.collect()
}
use xml_util::parse_argb_color;
fn cell_numeric_value(cell: &umya_spreadsheet::Cell) -> Option<f64> {
let raw = cell.get_raw_value().to_string();
if let Ok(v) = raw.parse::<f64>() {
return Some(v);
}
cell.get_value().to_string().parse::<f64>().ok()
}
fn evaluate_cell_is_rule(
cell_val: f64,
operator: &umya_spreadsheet::ConditionalFormattingOperatorValues,
rule: &umya_spreadsheet::ConditionalFormattingRule,
) -> bool {
use umya_spreadsheet::ConditionalFormattingOperatorValues::*;
let formula_val = rule.get_formula().and_then(|f| {
let s = f.get_address_str();
s.trim().parse::<f64>().ok()
});
let Some(threshold) = formula_val else {
return false;
};
match operator {
GreaterThan => cell_val > threshold,
GreaterThanOrEqual => cell_val >= threshold,
LessThan => cell_val < threshold,
LessThanOrEqual => cell_val <= threshold,
Equal => (cell_val - threshold).abs() < f64::EPSILON,
NotEqual => (cell_val - threshold).abs() >= f64::EPSILON,
Between => cell_val >= threshold,
NotBetween => cell_val < threshold,
_ => false,
}
}
fn extract_cond_fmt_style(rule: &umya_spreadsheet::ConditionalFormattingRule) -> CondFmtOverride {
let mut result = CondFmtOverride::default();
if let Some(style) = rule.get_style() {
if let Some(bg) = style.get_background_color() {
result.background = parse_argb_color(bg.get_argb());
}
if let Some(font) = style.get_font() {
if *font.get_bold() {
result.bold = Some(true);
}
let color_argb = font.get_color().get_argb();
if !color_argb.is_empty() && color_argb != "FF000000" {
result.font_color = parse_argb_color(color_argb);
}
}
}
result
}
fn parse_umya_color_argb(color: &umya_spreadsheet::Color) -> Option<Color> {
let argb = color.get_argb();
if argb.is_empty() {
return None;
}
parse_argb_color(argb)
}
fn interpolate_color(color_a: Color, color_b: Color, ratio: f64) -> Color {
let ratio = ratio.clamp(0.0, 1.0);
let r = (color_a.r as f64 + (color_b.r as f64 - color_a.r as f64) * ratio).round() as u8;
let g = (color_a.g as f64 + (color_b.g as f64 - color_a.g as f64) * ratio).round() as u8;
let b = (color_a.b as f64 + (color_b.b as f64 - color_a.b as f64) * ratio).round() as u8;
Color::new(r, g, b)
}
fn collect_numeric_values_in_ranges(
sheet: &umya_spreadsheet::Worksheet,
ranges: &[CellRange],
) -> Vec<f64> {
let mut values = Vec::new();
for range in ranges {
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = sheet.get_cell((col, row))
&& let Some(val) = cell_numeric_value(cell)
{
values.push(val);
}
}
}
}
values
}
fn compute_min_max(values: &[f64]) -> Option<(f64, f64, f64)> {
if values.is_empty() {
return None;
}
let min_val: f64 = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max_val: f64 = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let val_range: f64 = max_val - min_val;
Some((min_val, max_val, val_range))
}
fn apply_cell_is_rule(
sheet: &umya_spreadsheet::Worksheet,
rule: &umya_spreadsheet::ConditionalFormattingRule,
ranges: &[CellRange],
overrides: &mut HashMap<CellPos, CondFmtOverride>,
) {
let operator = rule.get_operator();
let fmt = extract_cond_fmt_style(rule);
for range in ranges {
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = sheet.get_cell((col, row))
&& let Some(val) = cell_numeric_value(cell)
&& evaluate_cell_is_rule(val, operator, rule)
{
let entry = overrides.entry((col, row)).or_default();
if fmt.background.is_some() {
entry.background = fmt.background;
}
if fmt.font_color.is_some() {
entry.font_color = fmt.font_color;
}
if fmt.bold.is_some() {
entry.bold = fmt.bold;
}
}
}
}
}
}
fn apply_color_scale_rule(
sheet: &umya_spreadsheet::Worksheet,
rule: &umya_spreadsheet::ConditionalFormattingRule,
ranges: &[CellRange],
overrides: &mut HashMap<CellPos, CondFmtOverride>,
) {
let Some(cs) = rule.get_color_scale() else {
return;
};
let colors: Vec<Option<Color>> = cs
.get_color_collection()
.iter()
.map(parse_umya_color_argb)
.collect();
if colors.len() < 2 {
return;
}
let numeric_vals: Vec<f64> = collect_numeric_values_in_ranges(sheet, ranges);
let Some((min_val, _max_val, val_range)) = compute_min_max(&numeric_vals) else {
return;
};
let color_min: Color = colors[0].unwrap_or(Color::white());
let color_max: Color = colors[colors.len() - 1].unwrap_or(Color::black());
for range in ranges {
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = sheet.get_cell((col, row))
&& let Some(val) = cell_numeric_value(cell)
{
let ratio: f64 = if val_range.abs() < f64::EPSILON {
0.5
} else {
(val - min_val) / val_range
};
let color: Color = if colors.len() == 3 {
let color_mid: Color = colors[1].unwrap_or(Color::new(255, 255, 0));
if ratio <= 0.5 {
interpolate_color(color_min, color_mid, ratio * 2.0)
} else {
interpolate_color(color_mid, color_max, (ratio - 0.5) * 2.0)
}
} else {
interpolate_color(color_min, color_max, ratio)
};
let entry = overrides.entry((col, row)).or_default();
entry.background = Some(color);
}
}
}
}
}
fn apply_data_bar_rule(
sheet: &umya_spreadsheet::Worksheet,
rule: &umya_spreadsheet::ConditionalFormattingRule,
ranges: &[CellRange],
overrides: &mut HashMap<CellPos, CondFmtOverride>,
) {
let Some(db) = rule.get_data_bar() else {
return;
};
let bar_color: Color = db
.get_color_collection()
.first()
.and_then(parse_umya_color_argb)
.unwrap_or(Color::new(0x63, 0x8E, 0xC6));
let numeric_vals: Vec<f64> = collect_numeric_values_in_ranges(sheet, ranges);
let Some((min_val, _max_val, val_range)) = compute_min_max(&numeric_vals) else {
return;
};
for range in ranges {
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = sheet.get_cell((col, row))
&& let Some(val) = cell_numeric_value(cell)
{
let pct: f64 = if val_range.abs() < f64::EPSILON {
50.0
} else {
((val - min_val) / val_range) * 100.0
};
let entry = overrides.entry((col, row)).or_default();
entry.data_bar = Some(DataBarInfo {
color: bar_color,
fill_pct: pct,
});
}
}
}
}
}
fn apply_icon_set_rule(
sheet: &umya_spreadsheet::Worksheet,
rule: &umya_spreadsheet::ConditionalFormattingRule,
ranges: &[CellRange],
overrides: &mut HashMap<CellPos, CondFmtOverride>,
) {
let numeric_vals: Vec<f64> = collect_numeric_values_in_ranges(sheet, ranges);
let Some((min_val, _max_val, val_range)) = compute_min_max(&numeric_vals) else {
return;
};
let cfvo_thresholds: Vec<f64> = rule
.get_icon_set()
.map(|is| is.get_cfvo_collection())
.unwrap_or(&[])
.iter()
.filter_map(|cfvo| {
let pct: f64 = cfvo.get_val().parse().ok()?;
Some(min_val + val_range * (pct / 100.0))
})
.collect();
let thresholds: Vec<f64> = if cfvo_thresholds.len() >= 2 {
cfvo_thresholds
} else {
vec![
min_val,
min_val + val_range / 3.0,
min_val + val_range * 2.0 / 3.0,
]
};
let icons: &[&str] = if thresholds.len() >= 5 {
&["⇊", "↓", "→", "↑", "⇈"]
} else {
&["↓", "→", "↑"]
};
for range in ranges {
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = sheet.get_cell((col, row))
&& let Some(val) = cell_numeric_value(cell)
{
let icon_idx: usize = evaluate_icon_index(val, &thresholds, icons.len());
let entry = overrides.entry((col, row)).or_default();
entry.icon_text = Some(icons[icon_idx].to_string());
}
}
}
}
}
pub(crate) fn build_cond_fmt_overrides(
sheet: &umya_spreadsheet::Worksheet,
) -> HashMap<(u32, u32), CondFmtOverride> {
let mut overrides: HashMap<CellPos, CondFmtOverride> = HashMap::new();
for cf in sheet.get_conditional_formatting_collection() {
let sqref = cf.get_sequence_of_references().get_sqref();
let ranges: Vec<CellRange> = parse_sqref(&sqref);
if ranges.is_empty() {
continue;
}
for rule in cf.get_conditional_collection() {
use umya_spreadsheet::ConditionalFormatValues;
match rule.get_type() {
ConditionalFormatValues::CellIs => {
apply_cell_is_rule(sheet, rule, &ranges, &mut overrides);
}
ConditionalFormatValues::ColorScale => {
apply_color_scale_rule(sheet, rule, &ranges, &mut overrides);
}
ConditionalFormatValues::DataBar => {
apply_data_bar_rule(sheet, rule, &ranges, &mut overrides);
}
ConditionalFormatValues::IconSet => {
apply_icon_set_rule(sheet, rule, &ranges, &mut overrides);
}
_ => {}
}
}
}
overrides
}
fn evaluate_icon_index(val: f64, thresholds: &[f64], num_icons: usize) -> usize {
if num_icons == 0 {
return 0;
}
for i in (1..thresholds.len()).rev() {
if val >= thresholds[i] {
return (i).min(num_icons - 1);
}
}
0
}
#[cfg(test)]
#[path = "cond_fmt_tests.rs"]
mod tests;