use crate::computation::rational::{
checked_div, checked_mul, decimal_to_display_str, NumericFailure,
};
use crate::literals::rational_from_parsed_decimal;
use crate::planning::semantics::{
format_decimal_for_api, range_element_type_specification, ratio_element_type_for_api,
semantic_calendar_unit_from_measure_type, LemmaType, LiteralUnitMapFailure, LiteralValue,
SemanticDateTime, SemanticTime, TypeSpecification, UnitFactorSource, ValueKind,
};
use crate::planning::unit_family::{declared_bare_names_only, FamilyUnitCatalog, FamilyUnitEntry};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CalendarResult {
pub value: Decimal,
pub unit: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RangeResult {
pub from: RuleResultValue,
pub to: RuleResultValue,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RuleResultValue {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measure: Option<BTreeMap<String, Decimal>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ratio: Option<BTreeMap<String, Decimal>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub number: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boolean: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<SemanticDateTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<SemanticTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calendar: Option<CalendarResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<Box<RangeResult>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spec: Option<Box<SpecResult>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SpecResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
pub spec: String,
pub effective: String,
pub instance: String,
pub data: BTreeMap<String, SpecRuleResult>,
pub rules: BTreeMap<String, SpecRuleResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SpecRuleResult {
pub vetoed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub veto_reason: Option<String>,
pub rule_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<RuleResultValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleResultValueFailure {
DecimalLimit,
NumericOverflow,
OutOfMemory,
}
pub(crate) enum UnitExpansion<'a> {
Declared,
Family(&'a FamilyUnitEntry),
}
pub fn rule_result_value_failure_message(failure: RuleResultValueFailure) -> &'static str {
match failure {
RuleResultValueFailure::DecimalLimit => "Calculated result exceeds decimal value limit",
RuleResultValueFailure::NumericOverflow => "numeric overflow",
RuleResultValueFailure::OutOfMemory => "out of memory",
}
}
fn map_numeric_to_rule_result_value_failure(failure: NumericFailure) -> RuleResultValueFailure {
match failure {
NumericFailure::Overflow => RuleResultValueFailure::DecimalLimit,
NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
NumericFailure::DivisionByZero => {
panic!(
"BUG: decimal commit encountered division by zero while building RuleResultValue"
)
}
NumericFailure::Irrational => {
panic!(
"BUG: decimal commit encountered irrational result while building RuleResultValue"
)
}
}
}
fn map_unit_conversion_failure(failure: NumericFailure) -> RuleResultValueFailure {
match failure {
NumericFailure::Overflow => RuleResultValueFailure::NumericOverflow,
NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
NumericFailure::DivisionByZero => {
panic!("BUG: unit conversion division by zero while building RuleResultValue")
}
NumericFailure::Irrational => {
panic!("BUG: unit conversion irrational while building RuleResultValue")
}
}
}
fn map_literal_unit_map_failure(failure: LiteralUnitMapFailure) -> RuleResultValueFailure {
match failure {
LiteralUnitMapFailure::Commit(failure) => map_numeric_to_rule_result_value_failure(failure),
LiteralUnitMapFailure::UnitConversion(failure) => map_unit_conversion_failure(failure),
}
}
fn expansion_for_type<'a>(
rule_type: &LemmaType,
catalog: Option<&'a FamilyUnitCatalog>,
) -> UnitExpansion<'a> {
let Some(catalog) = catalog else {
return UnitExpansion::Declared;
};
match catalog.entry_for_type(rule_type) {
Some(entry) => UnitExpansion::Family(entry),
None => UnitExpansion::Declared,
}
}
fn unit_names_for_expansion(
result_type: &LemmaType,
expansion: &UnitExpansion<'_>,
catalog: Option<&FamilyUnitCatalog>,
) -> Arc<[String]> {
match expansion {
UnitExpansion::Declared => Arc::from(declared_bare_names_only(result_type)),
UnitExpansion::Family(_) => catalog
.expect("BUG: family expansion requires FamilyUnitCatalog")
.ordered_bare_names_for_type(result_type),
}
}
fn measure_factor_source<'a>(
result_type: &'a LemmaType,
expansion: &'a UnitExpansion<'a>,
) -> UnitFactorSource<'a> {
match expansion {
UnitExpansion::Declared => UnitFactorSource::DeclaredOn(result_type),
UnitExpansion::Family(entry) => UnitFactorSource::Merged {
measure: entry.merged_measure_units.as_ref(),
ratio: entry.merged_ratio_units.as_ref(),
},
}
}
fn ratio_factor_source<'a>(
result_type: &'a LemmaType,
expansion: &'a UnitExpansion<'a>,
) -> UnitFactorSource<'a> {
measure_factor_source(result_type, expansion)
}
fn result_value_from_literal(
literal: &LiteralValue,
lemma_type: &LemmaType,
expansion: &UnitExpansion<'_>,
catalog: Option<&FamilyUnitCatalog>,
) -> Result<RuleResultValue, RuleResultValueFailure> {
match &literal.value {
ValueKind::Range(from, to) => {
let from_value = result_value_from_range_endpoint(
&from.to_literal(),
from.lemma_type.as_ref(),
expansion,
catalog,
)?;
let to_value = result_value_from_range_endpoint(
&to.to_literal(),
to.lemma_type.as_ref(),
expansion,
catalog,
)?;
Ok(RuleResultValue {
result: Some(literal.display_value_with_type(lemma_type)),
range: Some(Box::new(RangeResult {
from: from_value,
to: to_value,
})),
..RuleResultValue::default()
})
}
_ => result_value_from_non_range_literal(literal, lemma_type, expansion, catalog),
}
}
pub(crate) fn rule_result_value_from_literal(
literal: &LiteralValue,
rule_type: &LemmaType,
catalog: &FamilyUnitCatalog,
) -> Result<RuleResultValue, RuleResultValueFailure> {
let expansion = expansion_for_type(rule_type, Some(catalog));
result_value_from_literal(literal, rule_type, &expansion, Some(catalog))
}
pub fn type_scoped_result_value_from_literal(
literal: &LiteralValue,
lemma_type: &LemmaType,
) -> Result<RuleResultValue, RuleResultValueFailure> {
let expansion = UnitExpansion::Declared;
result_value_from_literal(literal, lemma_type, &expansion, None)
}
fn result_value_from_range_endpoint(
endpoint: &LiteralValue,
endpoint_type: &LemmaType,
expansion: &UnitExpansion<'_>,
catalog: Option<&FamilyUnitCatalog>,
) -> Result<RuleResultValue, RuleResultValueFailure> {
if matches!(&endpoint.value, ValueKind::Range(_, _)) {
panic!("BUG: range endpoint must not itself be a range");
}
result_value_from_non_range_literal(endpoint, endpoint_type, expansion, catalog)
}
fn result_value_from_non_range_literal(
literal: &LiteralValue,
result_type: &LemmaType,
expansion: &UnitExpansion<'_>,
catalog: Option<&FamilyUnitCatalog>,
) -> Result<RuleResultValue, RuleResultValueFailure> {
match &literal.value {
ValueKind::Measure(rational) if result_type.is_calendar_like() => {
let unit = semantic_calendar_unit_from_measure_type(result_type);
let value = result_type
.try_rational_as_decimal(rational)
.map_err(map_numeric_to_rule_result_value_failure)?;
let result = Some(literal.display_value_with_type(result_type));
Ok(RuleResultValue {
result,
calendar: Some(CalendarResult {
value,
unit: unit.to_string(),
}),
..RuleResultValue::default()
})
}
ValueKind::Measure(_) => {
let result = Some(literal.display_value_with_type(result_type));
let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
Ok(RuleResultValue {
result,
measure: Some(
result_type
.measure_literal_unit_map(
literal,
&unit_name_refs,
measure_factor_source(result_type, expansion),
)
.map_err(map_literal_unit_map_failure)?,
),
unit: result_type.unit().map(str::to_string),
..RuleResultValue::default()
})
}
ValueKind::Ratio(_) => {
let result = Some(literal.display_value_with_type(result_type));
let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
Ok(RuleResultValue {
result,
ratio: Some(
result_type
.ratio_literal_unit_map(
literal,
&unit_name_refs,
ratio_factor_source(result_type, expansion),
)
.map_err(map_literal_unit_map_failure)?,
),
unit: result_type.unit().map(str::to_string),
..RuleResultValue::default()
})
}
other => scalar_result_value(result_type, other),
}
}
fn scalar_result_value(
result_type: &LemmaType,
value: &ValueKind,
) -> Result<RuleResultValue, RuleResultValueFailure> {
match value {
ValueKind::Number(rational) => {
let decimal = rational
.try_to_decimal()
.map_err(map_numeric_to_rule_result_value_failure)?;
let api_decimal = format_decimal_for_api(decimal, result_type.decimal_places());
let display_string = decimal_to_display_str(&decimal);
Ok(RuleResultValue {
result: Some(display_string),
number: Some(api_decimal),
..RuleResultValue::default()
})
}
ValueKind::Boolean(b) => {
let result = Some(value.to_string());
Ok(RuleResultValue {
result,
boolean: Some(*b),
..RuleResultValue::default()
})
}
ValueKind::Text(_) | ValueKind::Date(_) | ValueKind::Time(_) => {
let result = Some(value.to_string());
Ok(scalar_result_value_non_numeric(result_type, result, value))
}
ValueKind::Measure(_) | ValueKind::Ratio(_) => {
unreachable!("BUG: measure and ratio must be handled by caller")
}
ValueKind::Range(_, _) => {
unreachable!("BUG: range must be handled by result_value_from_literal")
}
ValueKind::Spec(instance) => {
let identity = result_type.spec_identity().unwrap_or_else(|| {
panic!(
"BUG: spec value whose type is not a spec ({})",
result_type.name()
)
});
Ok(RuleResultValue {
result: Some(instance.to_string()),
spec: Some(Box::new(SpecResult {
repository: identity.repository.clone(),
spec: identity.spec.clone(),
effective: identity.effective.to_string(),
instance: instance.to_string(),
data: BTreeMap::new(),
rules: BTreeMap::new(),
})),
..RuleResultValue::default()
})
}
}
}
fn scalar_result_value_non_numeric(
_result_type: &LemmaType,
result: Option<String>,
value: &ValueKind,
) -> RuleResultValue {
match value {
ValueKind::Text(s) => RuleResultValue {
result,
text: Some(s.clone()),
..RuleResultValue::default()
},
ValueKind::Date(d) => RuleResultValue {
result,
date: Some(d.clone()),
..RuleResultValue::default()
},
ValueKind::Time(t) => RuleResultValue {
result,
time: Some(t.clone()),
..RuleResultValue::default()
},
_ => unreachable!("BUG: scalar_result_value_non_numeric called with non-scalar type"),
}
}
fn literal_from_measure_map(
measure: &BTreeMap<String, Decimal>,
rule_type: &LemmaType,
) -> LiteralValue {
let unit_names = rule_type
.measure_unit_names()
.expect("BUG: measure rule result must have declared units");
let unit_name = rule_type
.unit()
.map(str::to_string)
.or_else(|| unit_names.first().map(|name| (*name).to_string()))
.expect("BUG: measure rule result type must declare at least one unit");
let magnitude = measure
.get(&unit_name)
.unwrap_or_else(|| panic!("BUG: measure map missing unit '{unit_name}'"));
let rational = rational_from_parsed_decimal(*magnitude)
.expect("BUG: measure rule result value must lift to rational");
let factor = rule_type.measure_unit_factor(&unit_name);
let canonical = checked_mul(&rational, factor).unwrap_or_else(|failure| {
panic!("BUG: measure canonicalization from RuleResultValue fields failed: {failure}")
});
let typed = if rule_type.unit() == Some(unit_name.as_str()) {
Arc::new(rule_type.clone())
} else {
Arc::new(rule_type.clone().with_unit(unit_name))
};
LiteralValue::measure_with_type(canonical, typed)
}
fn literal_from_ratio_map(
ratio: &BTreeMap<String, Decimal>,
rule_type: &LemmaType,
) -> LiteralValue {
let ratio_type = ratio_element_type_for_api(rule_type);
let units = match &ratio_type.specifications {
TypeSpecification::Ratio { units, .. } => units,
_ => panic!(
"BUG: ratio rule result type must be Ratio, got {}",
rule_type.name()
),
};
let unit = rule_type
.unit()
.and_then(|name| units.get(name).ok())
.or_else(|| units.iter().next())
.expect("BUG: ratio rule result type must declare at least one unit");
let magnitude = ratio
.get(&unit.name)
.unwrap_or_else(|| panic!("BUG: ratio map missing unit '{}'", unit.name));
let display_rational = rational_from_parsed_decimal(*magnitude)
.expect("BUG: ratio rule result value must lift to rational");
let canonical = checked_div(&display_rational, &unit.value).unwrap_or_else(|failure| {
panic!("BUG: ratio canonicalization from RuleResultValue fields failed: {failure}")
});
let typed = if rule_type.unit() == Some(unit.name.as_str()) {
Arc::new(rule_type.clone())
} else {
Arc::new(rule_type.clone().with_unit(unit.name.clone()))
};
LiteralValue::ratio_with_type(canonical, typed)
}
impl RuleResultValue {
pub fn to_literal(&self, rule_type: &LemmaType) -> LiteralValue {
if let Some(spec) = &self.spec {
let hops: Vec<&str> = spec
.instance
.split('.')
.filter(|hop| !hop.is_empty())
.collect();
if hops.len() != 1 {
todo!(
"rebuilding spec instance '{}' needs each uses hop's spec",
spec.instance
);
}
let identity = rule_type
.spec_identity()
.unwrap_or_else(|| panic!("BUG: spec result whose rule type is not a spec"));
return LiteralValue::new(crate::planning::semantics::ValueKind::Spec(
crate::planning::semantics::SpecInstance {
prefix: vec![crate::planning::semantics::PathSegment {
uses: hops[0].to_string(),
repository: identity.repository.clone(),
spec: identity.spec.clone(),
}],
},
));
}
if let Some(range) = &self.range {
if range.from.range.is_some() || range.to.range.is_some() {
panic!("BUG: range endpoint must not itself be a range");
}
let endpoint_schema =
element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
let left_type =
endpoint_type_with_written_unit(&endpoint_schema, range.from.unit.as_deref());
let right_type =
endpoint_type_with_written_unit(&endpoint_schema, range.to.unit.as_deref());
let left = range.from.to_literal(&left_type);
let right = range.to.to_literal(&right_type);
return LiteralValue::range(
crate::planning::semantics::TypedLiteral {
value: left.value,
lemma_type: Arc::new(left_type),
},
crate::planning::semantics::TypedLiteral {
value: right.value,
lemma_type: Arc::new(right_type),
},
);
}
if let Some(b) = self.boolean {
return LiteralValue::from_bool(b);
}
let owned_rule_type = match self.unit.as_deref() {
Some(unit)
if matches!(
&rule_type.specifications,
TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
) && rule_type.unit() != Some(unit) =>
{
Arc::new(rule_type.clone().with_unit(unit))
}
_ => Arc::new(rule_type.clone()),
};
if let Some(number) = self.number {
return LiteralValue::number_with_type_from_decimal(number, owned_rule_type);
}
if let Some(calendar) = &self.calendar {
let rational = rational_from_parsed_decimal(calendar.value)
.expect("BUG: calendar rule result value must lift to rational");
return LiteralValue::measure_with_type(rational, owned_rule_type);
}
if let Some(measure) = &self.measure {
return literal_from_measure_map(measure, owned_rule_type.as_ref());
}
if let Some(ratio) = &self.ratio {
return literal_from_ratio_map(ratio, owned_rule_type.as_ref());
}
if let Some(date) = &self.date {
return LiteralValue::date_with_type(date.clone(), owned_rule_type);
}
if let Some(time) = &self.time {
return LiteralValue::time_with_type(time.clone(), owned_rule_type);
}
if let Some(text) = &self.text {
return LiteralValue::text_with_type(text.clone(), owned_rule_type);
}
panic!("BUG: rule result value fields cannot reconstruct literal");
}
}
fn endpoint_type_with_written_unit(endpoint_schema: &LemmaType, unit: Option<&str>) -> LemmaType {
match unit {
Some(unit)
if matches!(
&endpoint_schema.specifications,
TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
) && endpoint_schema.unit() != Some(unit) =>
{
endpoint_schema.clone().with_unit(unit)
}
_ => endpoint_schema.clone(),
}
}
fn element_type_from_range_rule(rule_type: &LemmaType) -> Option<LemmaType> {
range_element_type_specification(&rule_type.specifications).map(LemmaType::primitive)
}
fn format_unit_map(map: &BTreeMap<String, Decimal>) -> String {
map.iter()
.map(|(unit, value)| format!("{value} {unit}"))
.collect::<Vec<_>>()
.join(", ")
}
impl fmt::Display for RuleResultValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(range) = &self.range {
return write!(f, "{}...{}", range.from, range.to);
}
if let Some(measure) = &self.measure {
return write!(f, "{}", format_unit_map(measure));
}
if let Some(ratio) = &self.ratio {
return write!(f, "{}", format_unit_map(ratio));
}
if let Some(number) = &self.number {
return write!(f, "{number}");
}
if let Some(b) = self.boolean {
return write!(f, "{b}");
}
if let Some(text) = &self.text {
return write!(f, "{text}");
}
if let Some(date) = &self.date {
return write!(f, "{date}");
}
if let Some(time) = &self.time {
return write!(f, "{time}");
}
if let Some(calendar) = &self.calendar {
return write!(f, "{} {}", calendar.value, calendar.unit);
}
if let Some(display) = &self.result {
return write!(f, "{display}");
}
write!(f, "")
}
}