///|
/// A recoverable validation error; no user input is inserted as raw XML.
pub suberror OdsError {
Invalid(String)
} derive(Debug)
///|
/// Formula cache values have the same types as literal values.
/// Date is a Gregorian ODF date in 1..9999, independent of an application's
/// serial date epoch. It does not promise Excel supports historical dates.
pub(all) enum Value {
Empty
Text(String)
Number(Double)
Boolean(Bool)
Date(Int, Int, Int)
} derive(Eq, Debug)
///|
struct Cell {
value : Value
formula : String?
style : Style
}
///|
/// Validate ODF values; application-specific date ranges are not inferred.
pub fn Cell::new(value : Value) -> Cell raise OdsError {
validate_value(value)
{ value, formula: None, style: Plain, }
}
///|
/// Optional date policy for Excel's 1900 date system, with an ISO date style.
/// Rejects dates before 1900-01-01 and all invalid Gregorian dates, including
/// Excel's fictional 1900-02-29. Does not configure a reader's date system or
/// guarantee rendering. For historical display, callers may explicitly use
/// Text("0001-01-01"), which has no date arithmetic semantics.
pub fn Cell::excel_1900_date(
year : Int,
month : Int,
day : Int,
) -> Cell raise OdsError {
let cell = Cell::new(Date(year, month, day))
if year < 1900 {
raise Invalid("Excel 1900 date policy requires 1900-01-01 or later")
}
cell.styled(DateISO)
}
///|
/// OpenFormula text must begin of:=. This validates storage, not formula syntax.
pub fn Cell::formula(expression : String, cache : Value) -> Cell raise OdsError {
validate_text(expression)
if !expression.has_prefix("of:=") || expression.length() <= 4 {
raise Invalid("formula must be nonempty OpenFormula text beginning of:=")
}
validate_value(cache)
if cache == Empty {
raise Invalid("formula requires a typed nonempty cache")
}
{ value: cache, formula: Some(expression), style: Plain, }
}
///|
pub fn Cell::value(self : Cell) -> Value {
self.value
}
///|
pub fn Cell::expression(self : Cell) -> String? {
self.formula
}
///|
fn validate_text(s : String) -> Unit raise OdsError {
if s.length() > 32767 {
raise Invalid("text exceeds 32767 UTF-16 units")
}
for c in s {
let n = c.to_int()
if !(n == 9 ||
n == 10 ||
n == 13 ||
(n >= 32 && n <= 0xD7FF) ||
(n >= 0xE000 && n <= 0xFFFD) ||
(n >= 0x10000 && n <= 0x10FFFF)) {
raise Invalid("text contains an XML 1.0 forbidden character")
}
}
}
///|
fn validate_value(v : Value) -> Unit raise OdsError {
match v {
Empty | Boolean(_) => ()
Text(s) => validate_text(s)
Number(n) =>
if n.is_nan() || n.is_inf() {
raise Invalid("number must be finite")
}
Date(y, m, d) => {
if y < 1 || y > 9999 || m < 1 || m > 12 {
raise Invalid("date is outside Gregorian range")
}
let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
let days = match m {
2 => if leap { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
if d < 1 || d > days {
raise Invalid("invalid Gregorian day")
}
}
}
}
///|
pub extend Value with Eq::{equal, not_equal}
///|
pub extend Value with @debug.Debug::{to_repr}
///|
pub extend OdsError with @debug.Debug::{to_repr}