///|
pub(all) struct Sample {
time : Int
value : Double
} derive(Debug)
///|
pub(all) struct Series {
name : String
samples : Array[Sample]
} derive(Debug)
///|
pub(all) struct Summary {
count : Int
min : Double
max : Double
mean : Double
range : Double
variance : Double
std_dev : Double
} derive(Debug)
///|
pub fn Sample::new(time : Int, value : Double) -> Sample {
{ time, value }
}
///|
pub fn Series::new(name : String, samples : Array[Sample]) -> Series {
{ name, samples }
}
///|
pub fn Series::length(self : Series) -> Int {
self.samples.length()
}
///|
pub fn Series::is_empty(self : Series) -> Bool {
self.samples.length() == 0
}
///|
pub fn Series::sum(self : Series) -> Double {
let mut total = 0.0
for i = 0; i < self.samples.length(); i = i + 1 {
total = total + self.samples[i].value
}
total
}
///|
pub fn Series::min_value(self : Series) -> Double {
if self.samples.length() == 0 {
return 0.0
}
let mut best = self.samples[0].value
for i = 1; i < self.samples.length(); i = i + 1 {
if self.samples[i].value < best {
best = self.samples[i].value
}
}
best
}
///|
pub fn Series::max_value(self : Series) -> Double {
if self.samples.length() == 0 {
return 0.0
}
let mut best = self.samples[0].value
for i = 1; i < self.samples.length(); i = i + 1 {
if self.samples[i].value > best {
best = self.samples[i].value
}
}
best
}
///|
pub fn Series::mean(self : Series) -> Double {
if self.samples.length() == 0 {
0.0
} else {
self.sum() / self.samples.length().to_double()
}
}
///|
pub fn Series::summary(self : Series) -> Summary {
let variance = self.variance()
{
count: self.samples.length(),
min: self.min_value(),
max: self.max_value(),
mean: self.mean(),
range: self.range(),
variance,
std_dev: variance.sqrt(),
}
}
///|
pub fn Series::range(self : Series) -> Double {
self.max_value() - self.min_value()
}
///|
pub fn Series::variance(self : Series) -> Double {
if self.samples.length() < 2 {
return 0.0
}
let mean = self.mean()
let mut total = 0.0
for i = 0; i < self.samples.length(); i = i + 1 {
let delta = self.samples[i].value - mean
total = total + delta * delta
}
total / self.samples.length().to_double()
}
///|
pub fn Series::normalize_minmax(self : Series, name? : String = "") -> Series {
let min = self.min_value()
let range = self.range()
let normalized : Array[Sample] = []
for i = 0; i < self.samples.length(); i = i + 1 {
let value = if range == 0.0 {
0.0
} else {
(self.samples[i].value - min) / range
}
normalized.push(Sample::new(self.samples[i].time, value))
}
Series::new(
if name == "" {
self.name + ".normalized"
} else {
name
},
normalized,
)
}
///|
pub fn Series::moving_average(
self : Series,
window : Int,
name? : String = "",
) -> Series {
let normalized_window = if window <= 1 { 1 } else { window }
let smoothed : Array[Sample] = []
let mut total = 0.0
for i = 0; i < self.samples.length(); i = i + 1 {
total = total + self.samples[i].value
if i >= normalized_window {
total = total - self.samples[i - normalized_window].value
}
let count = if i + 1 < normalized_window {
i + 1
} else {
normalized_window
}
smoothed.push(Sample::new(self.samples[i].time, total / count.to_double()))
}
Series::new(if name == "" { self.name + ".ma" } else { name }, smoothed)
}
///|
pub fn Series::exponential_smoothing(
self : Series,
alpha_per_mille : Int,
name? : String = "",
) -> Series {
if self.samples.length() == 0 {
return Series::new(if name == "" { self.name + ".ema" } else { name }, [])
}
let alpha = clamp_int(alpha_per_mille, 0, 1000).to_double() / 1000.0
let smoothed : Array[Sample] = []
let mut last = self.samples[0].value
smoothed.push(Sample::new(self.samples[0].time, last))
for i = 1; i < self.samples.length(); i = i + 1 {
last = alpha * self.samples[i].value + (1.0 - alpha) * last
smoothed.push(Sample::new(self.samples[i].time, last))
}
Series::new(if name == "" { self.name + ".ema" } else { name }, smoothed)
}
///|
pub fn Series::difference(self : Series, name? : String = "") -> Series {
let diff : Array[Sample] = []
if self.samples.length() < 2 {
return Series::new(
if name == "" {
self.name + ".diff"
} else {
name
},
diff,
)
}
for i = 1; i < self.samples.length(); i = i + 1 {
diff.push(
Sample::new(
self.samples[i].time,
self.samples[i].value - self.samples[i - 1].value,
),
)
}
Series::new(if name == "" { self.name + ".diff" } else { name }, diff)
}
///|
pub fn Series::rate_of_change(self : Series, name? : String = "") -> Series {
let rates : Array[Sample] = []
if self.samples.length() < 2 {
return Series::new(
if name == "" {
self.name + ".rate"
} else {
name
},
rates,
)
}
for i = 1; i < self.samples.length(); i = i + 1 {
let dt = self.samples[i].time - self.samples[i - 1].time
let rate = if dt == 0 {
0.0
} else {
(self.samples[i].value - self.samples[i - 1].value) / dt.to_double()
}
rates.push(Sample::new(self.samples[i].time, rate))
}
Series::new(if name == "" { self.name + ".rate" } else { name }, rates)
}
///|
pub fn Series::peaks(self : Series, threshold : Double) -> Array[Sample] {
let peaks : Array[Sample] = []
if self.samples.length() < 3 {
return peaks
}
for i = 1; i < self.samples.length() - 1; i = i + 1 {
let value = self.samples[i].value
if value >= threshold &&
value > self.samples[i - 1].value &&
value > self.samples[i + 1].value {
peaks.push(self.samples[i])
}
}
peaks
}
///|
pub fn Series::peak_count(self : Series, threshold : Double) -> Int {
self.peaks(threshold).length()
}
///|
pub fn Series::outliers(self : Series, z_threshold : Double) -> Array[Sample] {
let result : Array[Sample] = []
let summary = self.summary()
if summary.std_dev == 0.0 {
return result
}
let threshold = if z_threshold < 0.0 { -z_threshold } else { z_threshold }
for i = 0; i < self.samples.length(); i = i + 1 {
let delta = self.samples[i].value - summary.mean
let abs_delta = if delta < 0.0 { -delta } else { delta }
let z = abs_delta / summary.std_dev
if z >= threshold {
result.push(self.samples[i])
}
}
result
}
///|
pub fn Summary::to_json(self : Summary) -> String {
"{\"count\":\{self.count},\"min\":\{self.min},\"max\":\{self.max},\"mean\":\{self.mean},\"range\":\{self.range},\"variance\":\{self.variance},\"std_dev\":\{self.std_dev}}"
}
///|
pub fn Series::summary_json(self : Series) -> String {
self.summary().to_json()
}
///|
fn clamp_int(value : Int, min : Int, max : Int) -> Int {
if value < min {
min
} else if value > max {
max
} else {
value
}
}