///|
/// Runtime FEEL value. All temporal values are exact; functions capture their
/// defining scope in `callback`, which keeps the core value type independent
/// of the evaluator package.
pub(all) enum Value {
Null
Boolean(Bool)
Number(Decimal)
Text(String)
Date(Date)
Time(Time)
DateTime(DateTime)
DaysSeconds(DurationDS)
YearsMonths(DurationYM)
List(Array[Value])
Context(Map[String, Value])
Range(Range)
Function(FunctionValue)
}
///|
/// Callable FEEL value with parameter names and a captured callback.
pub(all) struct FunctionValue {
name : String
params : Array[String]
callback : (Array[Value]) -> Value raise EngineError
}
///|
/// Structural equality follows the FEEL `equals` rules, including temporal
/// instant comparison, so the `Eq` trait matches the rest of the engine.
pub impl Eq for Value with fn equal(self : Value, other : Value) {
self.equals(other)
}
///|
pub extend Value with Eq::{not_equal, equal}
///|
/// Return the FEEL runtime type name.
pub fn Value::type_name(self : Value) -> String {
match self {
Null => "null"
Boolean(_) => "boolean"
Number(_) => "number"
Text(_) => "string"
Date(_) => "date"
Time(_) => "time"
DateTime(_) => "date and time"
DaysSeconds(_) => "days and time duration"
YearsMonths(_) => "years and months duration"
List(_) => "list"
Context(_) => "context"
Range(_) => "range"
Function(_) => "function"
}
}
///|
/// Format a value as FEEL-oriented diagnostic text.
pub fn Value::text(self : Value) -> String {
match self {
Null => "null"
Boolean(b) => b.to_string()
Number(n) => n.text()
Text(s) => s
Date(d) => d.text()
Time(t) => t.text()
DateTime(d) => d.text()
DaysSeconds(d) => d.text()
YearsMonths(d) => d.text()
List(xs) => "[" + xs.map(x => x.text()).join(", ") + "]"
Context(xs) => {
let keys = xs.keys().to_array()
keys.sort()
"{" +
keys
.map(k => {
let body = match k.to_array() {
[] => k
[first, ..] =>
if (first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_' {
k
} else {
"\"" + k + "\""
}
}
body + ": " + xs[k].text()
})
.join(", ") +
"}"
}
Range(r) => r.text()
Function(f) => "function(" + f.params.join(", ") + ")"
}
}
///|
fn time_compare(a : Time, b : Time) -> Int32 raise EngineError {
match (a.zone, b.zone) {
(Unzoned, Unzoned) => a.seconds().compare(b.seconds())
(Unzoned, _) | (_, Unzoned) =>
raise fail("E_TYPE", "cannot compare zoned and unzoned times")
_ => {
let ao = match standard_zone_offset(a.zone) {
Some(o) => o
None => raise fail("E_ZONE_UNSUPPORTED", "unknown time zone")
}
let bo = match standard_zone_offset(b.zone) {
Some(o) => o
None => raise fail("E_ZONE_UNSUPPORTED", "unknown time zone")
}
(a.seconds() - ao.to_int64() * 1000000000L).compare(
b.seconds() - bo.to_int64() * 1000000000L,
)
}
}
}
///|
fn datetime_compare(a : DateTime, b : DateTime) -> Int32 raise EngineError {
match (a.has_offset(), b.has_offset()) {
(true, true) => a.epoch_nanos().compare(b.epoch_nanos())
(false, false) => {
let c = a.date.ordinal().compare(b.date.ordinal())
if c != 0 {
c
} else {
time_compare(a.time, b.time)
}
}
_ => raise fail("E_TYPE", "cannot compare zoned and unzoned date-times")
}
}
///|
/// Compare compatible FEEL values; incompatible kinds raise E_TYPE.
pub fn compare_values(a : Value, b : Value) -> Int32 raise EngineError {
match (a, b) {
(Number(x), Number(y)) => x.compare(y)
(Text(x), Text(y)) => {
let xs = x.to_array()
let ys = y.to_array()
for i = 0; i < xs.length() && i < ys.length(); i = i + 1 {
if xs[i] < ys[i] {
return -1
}
if xs[i] > ys[i] {
return 1
}
}
xs.length() - ys.length()
}
(Boolean(x), Boolean(y)) => if x == y { 0 } else if x { 1 } else { -1 }
(Date(x), Date(y)) => x.ordinal().compare(y.ordinal())
(Time(x), Time(y)) => time_compare(x, y)
(DateTime(x), DateTime(y)) => datetime_compare(x, y)
(DaysSeconds(x), DaysSeconds(y)) => x.total_nanos().compare(y.total_nanos())
(YearsMonths(x), YearsMonths(y)) =>
x.total_months().compare(y.total_months())
(List(xs), List(ys)) => {
let n = if xs.length() < ys.length() { xs.length() } else { ys.length() }
for i = 0; i < n; i = i + 1 {
let c = compare_values(xs[i], ys[i])
if c != 0 {
return c
}
}
xs.length().compare(ys.length())
}
_ =>
raise fail(
"E_TYPE",
"values of type " +
a.type_name() +
" and " +
b.type_name() +
" are not comparable",
)
}
}
///|
/// Compare FEEL values structurally, with temporal instant equality.
pub fn Value::equals(a : Value, b : Value) -> Bool {
match (a, b) {
(Null, Null) => true
(Boolean(x), Boolean(y)) => x == y
(Number(x), Number(y)) => x.compare(y) == 0
(Text(x), Text(y)) => x == y
(Date(x), Date(y)) =>
x.year == y.year && x.month == y.month && x.day == y.day
(Time(x), Time(y)) => time_equals(x, y)
(DateTime(x), DateTime(y)) => datetime_equals(x, y)
(DaysSeconds(x), DaysSeconds(y)) => x.total_nanos() == y.total_nanos()
(YearsMonths(x), YearsMonths(y)) => x.total_months() == y.total_months()
(List(xs), List(ys)) => {
if xs.length() != ys.length() {
return false
}
let mut same = true
for i, x in xs {
if !x.equals(ys[i]) {
same = false
}
}
same
}
(Context(xs), Context(ys)) => {
if xs.length() != ys.length() {
return false
}
let mut same = true
for k, v in xs {
match ys.get(k) {
Some(w) => if !v.equals(w) { same = false }
None => same = false
}
}
same
}
(Range(x), Range(y)) =>
x.comparison == y.comparison &&
x.start_included == y.start_included &&
x.end_included == y.end_included &&
eq_bound(x.start, y.start) &&
eq_bound(x.end, y.end)
_ => false
}
}
///|
fn time_equals(x : Time, y : Time) -> Bool {
if x.zone is Named(a) && y.zone is Named(b) && a == b {
return x.seconds() == y.seconds()
}
let xs = x.seconds()
let ys = y.seconds()
match (standard_zone_offset(x.zone), standard_zone_offset(y.zone)) {
(Some(a), Some(b)) =>
xs - a.to_int64() * 1000000000L == ys - b.to_int64() * 1000000000L
_ => false
}
}
///|
fn datetime_instant_seconds(dt : DateTime) -> Int64? {
let offset = datetime_zone_offset(dt) catch { _ => return None }
Some(
dt.date.ordinal() * 86400L +
dt.time.seconds() / 1000000000L -
offset.to_int64(),
)
}
///|
fn datetime_equals(x : DateTime, y : DateTime) -> Bool {
match (datetime_instant_seconds(x), datetime_instant_seconds(y)) {
(Some(a), Some(b)) => a == b && x.time.nanos == y.time.nanos
_ => false
}
}
///|
fn eq_bound(a : Value?, b : Value?) -> Bool {
match (a, b) {
(None, None) => true
(Some(x), Some(y)) => x.equals(y)
_ => false
}
}
///|
/// Encode a value as JSON; decimals retain exact numeric spelling.
pub fn Value::json(self : Value) -> Json raise EngineError {
match self {
Null => Json::null()
Boolean(b) => Json(b)
Number(n) =>
@json.parse(n.text(), preserve_number_repr=true) catch {
_ => raise fail("E_INTERNAL", "decimal JSON")
}
Text(s) => Json(s)
Date(d) => Json(d.text())
Time(t) => Json(t.text())
DateTime(d) => Json(d.text())
DaysSeconds(d) => Json(d.text())
YearsMonths(d) => Json(d.text())
List(xs) => Json::array(xs.map(x => x.json()))
Context(xs) => {
let out = Map([])
for k, v in xs {
out[k] = v.json()
}
Json::object(out)
}
Range(r) => r.json()
Function(_) => Json::null()
}
}
///|
/// Decode JSON values using exact decimals.
pub fn from_json(j : Json) -> Value raise EngineError {
match j {
Null => Null
True => Boolean(true)
False => Boolean(false)
Number(_, ..) => Number(decimal(j.stringify()))
String(s) => Text(s)
Array(xs) => List(xs.map(from_json))
Object(xs) => {
let out = Map([])
for k, v in xs {
out[k] = from_json(v)
}
Context(out)
}
}
}
///|
/// Decode a JSON object, rejecting invalid JSON and duplicate keys.
pub fn json_context(text : String) -> Map[String, Value] raise EngineError {
let j = @json.parse(
text,
preserve_number_repr=true,
reject_duplicate_keys=true,
) catch {
_ => raise fail("E_JSON", "invalid JSON input")
}
match from_json(j) {
Context(ctx) => ctx
_ => raise fail("E_INPUT_TYPE", "context must be a JSON object")
}
}