///|
/// Error categories are stable; messages add context, never change behavior.
pub(all) suberror RunoffError {
Shape(String)
Value(String)
Selection(String)
Numerical(String)
Parse(String)
} derive(Debug)
///|
pub extend RunoffError with @debug.Debug::{to_repr}
///|
/// An immutable-by-contract square cumulative paid-loss upper triangle.
pub struct Triangle {
priv rows : Array[Array[Double]]
}
///|
fn finite(x : Double) -> Bool {
x == x && x <= 1.7976931348623157e308 && x >= -1.7976931348623157e308
}
///|
fn checked(x : Double, context : String) -> Double raise RunoffError {
if !finite(x) {
raise Numerical(context)
}
x
}
///|
fn nonnegative(x : Double, context : String) -> Unit raise RunoffError {
let _ = checked(x, context)
if x < 0.0 {
raise Value(context)
}
}
///|
/// Construct from cumulative observations. Copies all nested arrays.
pub fn Triangle::new(rows : Array[Array[Double]]) -> Triangle raise RunoffError {
let n = rows.length()
if n < 2 || n > 128 {
raise Shape("triangle order must be 2..128")
}
let copy : Array[Array[Double]] = []
for i in 0.. Int32 {
self.rows.length()
}
///|
/// A defensive copy; modifying it cannot mutate this triangle.
pub fn Triangle::cumulative(self : Triangle) -> Array[Array[Double]] {
self.rows.map(fn(row) { row.copy() })
}
///|
pub fn Triangle::latest(self : Triangle) -> Array[Double] {
self.rows.map(fn(row) { row[row.length() - 1] })
}
///|
pub fn Triangle::observed_count(self : Triangle) -> Int32 {
let n = self.order()
n * (n + 1) / 2
}
///|
pub fn Triangle::at(
self : Triangle,
origin : Int32,
age : Int32,
) -> Double raise RunoffError {
if origin < 0 ||
origin >= self.order() ||
age < 0 ||
age >= self.order() - origin {
raise Shape("cell is outside observed triangle")
}
self.rows[origin][age]
}