use crate::schema::{FieldDef, FieldKind, FieldValue, NumericKind};
pub fn parse_field_value(field: &FieldDef, text: &str) -> Result<FieldValue, String> {
let text_field = matches!(field.kind, FieldKind::Text { .. } | FieldKind::Memo);
let zero_length = text_field && field.properties.allow_zero_length;
if text.trim().is_empty() {
if zero_length && field.properties.required {
return Ok(FieldValue::Text(String::new()));
}
return Ok(FieldValue::Null);
}
if text_field && text == "\"\"" {
if zero_length {
return Ok(FieldValue::Text(String::new()));
}
return Err(format!(
"field {:?} does not allow zero-length strings",
field.name
));
}
parse_value(&field.kind, text)
}
pub fn parse_value(kind: &FieldKind, text: &str) -> Result<FieldValue, String> {
if text.trim().is_empty() {
return Ok(FieldValue::Null);
}
match kind {
FieldKind::Text { .. } | FieldKind::Memo => Ok(FieldValue::Text(text.to_owned())),
FieldKind::Number(numeric) => parse_number(*numeric, text),
FieldKind::DateTime => parse_datetime(text).map(FieldValue::DateTime),
FieldKind::Currency => {
let scaled = parse_scaled(text, 4)?;
let value = i64::try_from(scaled)
.map_err(|_| format!("currency value is out of range: {text:?}"))?;
Ok(FieldValue::Currency(value))
}
FieldKind::AutoNumber(_) => {
Err("AutoNumber values are engine-assigned — leave the cell empty".to_owned())
}
FieldKind::Boolean { .. } => parse_boolean(text).map(FieldValue::Boolean),
FieldKind::Binary(_) => {
Err("binary values cannot be typed into a datasheet cell".to_owned())
}
FieldKind::MultiValue(_) => {
Err("multi-value values cannot be typed into a datasheet cell".to_owned())
}
FieldKind::Complex => {
Err("complex values cannot be typed into a datasheet cell".to_owned())
}
FieldKind::Guid => parse_guid(text).map(FieldValue::Guid),
}
}
pub fn row_values(fields: &[FieldDef], cells: &[String]) -> Result<Vec<FieldValue>, String> {
if cells.len() != fields.len() {
return Err(format!(
"expected {} cells, found {}",
fields.len(),
cells.len()
));
}
fields
.iter()
.zip(cells)
.map(|(field, cell)| {
parse_field_value(field, cell)
.map_err(|reason| format!("field {:?}: {reason}", field.name))
})
.collect()
}
fn parse_number(numeric: NumericKind, text: &str) -> Result<FieldValue, String> {
match numeric {
NumericKind::Byte | NumericKind::Integer | NumericKind::LongInteger => {
let parsed = text
.trim()
.parse::<i64>()
.map_err(|_| format!("expected an integer, found {text:?}"))?;
numeric
.validate_value(&FieldValue::Integer(parsed))
.map_err(|error| error.to_string())?;
Ok(FieldValue::Integer(parsed))
}
NumericKind::Single | NumericKind::Double => {
let parsed = text
.trim()
.parse::<f64>()
.map_err(|_| format!("expected a number, found {text:?}"))?;
if !parsed.is_finite() {
return Err(format!(
"floating-point values must be finite, found {text:?}"
));
}
Ok(FieldValue::Float(parsed))
}
NumericKind::Decimal { precision, scale } => {
let scaled = parse_scaled(text, scale)?;
check_precision(scaled, precision)?;
Ok(FieldValue::Decimal(scaled))
}
}
}
fn parse_scaled(text: &str, scale: u8) -> Result<i128, String> {
let trimmed = text.trim();
let (negative, unsigned) = match trimmed.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
};
let (int_part, frac_part) = match unsigned.split_once('.') {
Some((int_part, frac_part)) => (int_part, frac_part),
None => (unsigned, ""),
};
if (int_part.is_empty() && frac_part.is_empty())
|| !int_part.chars().all(|c| c.is_ascii_digit())
|| !frac_part.chars().all(|c| c.is_ascii_digit())
{
return Err(format!("expected a number, found {text:?}"));
}
if frac_part.len() > usize::from(scale) {
return Err(format!(
"at most {scale} decimal places, found {}",
frac_part.len()
));
}
let malformed = || format!("expected a number, found {text:?}");
let integer: i128 = if int_part.is_empty() {
0
} else {
int_part.parse().map_err(|_| malformed())?
};
let padded = format!(
"{frac_part}{}",
"0".repeat(usize::from(scale) - frac_part.len())
);
let fraction: i128 = if padded.is_empty() {
0
} else {
padded.parse().map_err(|_| malformed())?
};
let factor = 10i128
.checked_pow(u32::from(scale))
.ok_or_else(|| format!("value is out of range: {text:?}"))?;
let magnitude = integer
.checked_mul(factor)
.and_then(|value| value.checked_add(fraction))
.ok_or_else(|| format!("value is out of range: {text:?}"))?;
Ok(if negative { -magnitude } else { magnitude })
}
fn check_precision(scaled: i128, precision: u8) -> Result<(), String> {
let magnitude = scaled.unsigned_abs();
let digits = if magnitude == 0 {
1
} else {
magnitude.ilog10() + 1
};
if digits > u32::from(precision) {
return Err(format!("at most {precision} digits, found {digits}"));
}
Ok(())
}
fn parse_boolean(text: &str) -> Result<bool, String> {
match text.trim().to_ascii_lowercase().as_str() {
"true" | "yes" | "y" | "1" | "on" | "-1" => Ok(true),
"false" | "no" | "n" | "0" | "off" => Ok(false),
_ => Err(format!("expected a Yes/No value, found {text:?}")),
}
}
fn parse_guid(text: &str) -> Result<[u8; 16], String> {
let trimmed = text.trim().trim_start_matches('{').trim_end_matches('}');
let hex: String = trimmed.chars().filter(|c| *c != '-').collect();
if hex.len() != 32 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(format!("expected a GUID, found {text:?}"));
}
let mut bytes = [0u8; 16];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = u8::from_str_radix(&hex[index * 2..index * 2 + 2], 16)
.map_err(|_| format!("expected a GUID, found {text:?}"))?;
}
Ok(bytes)
}
fn parse_datetime(text: &str) -> Result<f64, String> {
let trimmed = text.trim();
let (date_part, time_part) = match trimmed.split_once([' ', 'T']) {
Some((date, time)) => (date, Some(time.trim())),
None => (trimmed, None),
};
let days = parse_date(date_part)?;
let fraction = match time_part {
Some(time) => parse_time(time)?,
None => 0.0,
};
Ok(days as f64 + fraction)
}
fn parse_date(text: &str) -> Result<i64, String> {
let mut parts = text.split('-');
let (Some(year), Some(month), Some(day), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(format!("expected a date as YYYY-MM-DD, found {text:?}"));
};
let malformed = || format!("expected a date as YYYY-MM-DD, found {text:?}");
let year: i64 = year.parse().map_err(|_| malformed())?;
let month: u32 = month.parse().map_err(|_| malformed())?;
let day: u32 = day.parse().map_err(|_| malformed())?;
if !(1..=12).contains(&month) {
return Err(format!("date month must be 1..=12, found {month}"));
}
let max_day = days_in_month(year, month);
if !(1..=max_day).contains(&day) {
return Err(format!(
"date {year:04}-{month:02} has 1..={max_day} days, found {day}"
));
}
Ok(days_from_civil(year, month, day) + 25_569)
}
fn parse_time(text: &str) -> Result<f64, String> {
let mut parts = text.split(':');
let (Some(hours), Some(minutes), seconds, None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(format!("expected a time as HH:MM[:SS], found {text:?}"));
};
let malformed = || format!("expected a time as HH:MM[:SS], found {text:?}");
let hours: u32 = hours.parse().map_err(|_| malformed())?;
let minutes: u32 = minutes.parse().map_err(|_| malformed())?;
let seconds: u32 = match seconds {
Some(seconds) => seconds.parse().map_err(|_| malformed())?,
None => 0,
};
if hours > 23 {
return Err(format!("time hours must be 0..=23, found {hours}"));
}
if minutes > 59 {
return Err(format!("time minutes must be 0..=59, found {minutes}"));
}
if seconds > 59 {
return Err(format!("time seconds must be 0..=59, found {seconds}"));
}
Ok(f64::from(hours * 3600 + minutes * 60 + seconds) / 86_400.0)
}
fn is_leap(year: i64) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
fn days_in_month(year: i64, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
if is_leap(year) {
29
} else {
28
}
}
}
}
fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let year = if month <= 2 { year - 1 } else { year };
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let month_prime = i64::from((month + 9) % 12);
let day_of_year = (153 * month_prime + 2) / 5 + i64::from(day) - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn leap_years_follow_the_gregorian_rule() {
assert!(is_leap(2000));
assert!(is_leap(2024));
assert!(!is_leap(1900));
assert!(!is_leap(2023));
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(2023, 2), 28);
assert_eq!(days_in_month(2024, 4), 30);
assert_eq!(days_in_month(2024, 12), 31);
}
#[test]
fn zero_decimal_values_are_one_digit() {
assert!(check_precision(0, 1).is_ok());
assert!(check_precision(0, 0).is_err());
}
#[test]
fn complex_kinds_are_not_typeable_cells() {
assert!(
parse_value(
&FieldKind::MultiValue(crate::schema::MultiValueKind::Text { max_len: 5 }),
"a"
)
.is_err()
);
assert!(parse_value(&FieldKind::Complex, "a").is_err());
}
}