mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 21:48:53 -06:00
This commit is contained in:
@@ -56,6 +56,10 @@ impl MonotoneCurve {
|
||||
}
|
||||
|
||||
pub fn eval(&self, x: f64) -> f64 {
|
||||
// NaN passes both bound checks below and would send the search past either end
|
||||
if x.is_nan() {
|
||||
return x;
|
||||
}
|
||||
let n = self.xs.len();
|
||||
if x <= self.xs[0] {
|
||||
return self.ys[0] + self.ms[0] * (x - self.xs[0]).min(0.0) * 0.0;
|
||||
@@ -76,8 +80,9 @@ impl MonotoneCurve {
|
||||
+ (t3 - t2) * h * self.ms[i + 1]
|
||||
}
|
||||
|
||||
/// Sample into a LUT of `n` entries over `0..1`.
|
||||
/// Sample into a LUT of `n` entries (at least 2) over `0..1`.
|
||||
pub fn to_lut(&self, n: usize) -> Lut1 {
|
||||
let n = n.max(2);
|
||||
Lut1 { v: (0..n).map(|i| self.eval(i as f64 / (n - 1) as f64) as f32).collect() }
|
||||
}
|
||||
|
||||
@@ -93,16 +98,24 @@ pub struct Lut1 {
|
||||
}
|
||||
|
||||
impl Lut1 {
|
||||
/// At least 2 entries, like [`Lut1::from_fn`].
|
||||
pub fn identity(n: usize) -> Lut1 {
|
||||
Lut1 { v: (0..n).map(|i| i as f32 / (n - 1) as f32).collect() }
|
||||
Lut1::from_fn(n, |x| x)
|
||||
}
|
||||
/// `f` sampled at `n` evenly spaced points over `0..1` (at least 2).
|
||||
pub fn from_fn(n: usize, f: impl Fn(f32) -> f32) -> Lut1 {
|
||||
let n = n.max(2);
|
||||
Lut1 { v: (0..n).map(|i| f(i as f32 / (n - 1) as f32)).collect() }
|
||||
}
|
||||
/// `v` is public, so a table shorter than 2 entries is read as constant (1) or the identity (0).
|
||||
#[inline]
|
||||
pub fn eval(&self, x: f32) -> f32 {
|
||||
let x = x.clamp(0.0, 1.0);
|
||||
let n = self.v.len();
|
||||
let f = x.clamp(0.0, 1.0) * (n - 1) as f32;
|
||||
if n < 2 {
|
||||
return self.v.first().copied().unwrap_or(x);
|
||||
}
|
||||
let f = x * (n - 1) as f32;
|
||||
let i = (f as usize).min(n - 2);
|
||||
let t = f - i as f32;
|
||||
self.v[i] + (self.v[i + 1] - self.v[i]) * t
|
||||
@@ -163,6 +176,22 @@ mod tests {
|
||||
assert!((c.eval(0.3) - 0.3).abs() < 2e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nan_input_and_short_luts_do_not_panic() {
|
||||
let c = MonotoneCurve::new(&[(0.0, 0.0), (0.5, 0.6), (1.0, 1.0)]);
|
||||
assert!(c.eval(f64::NAN).is_nan());
|
||||
assert!(c.eval(-f64::NAN).is_nan());
|
||||
for n in [0, 1] {
|
||||
assert_eq!(c.to_lut(n).v.len(), 2, "{n}");
|
||||
assert!(Lut1::identity(n).is_identity(), "{n}");
|
||||
assert_eq!(Lut1::from_fn(n, |x| x).v.len(), 2, "{n}");
|
||||
}
|
||||
assert_eq!(Lut1 { v: vec![] }.eval(0.3), 0.3);
|
||||
assert_eq!(Lut1 { v: vec![] }.eval(-1.0), 0.0, "inputs outside 0..1 clamp, as for any table");
|
||||
assert_eq!(Lut1 { v: vec![] }.eval(2.0), 1.0);
|
||||
assert_eq!(Lut1 { v: vec![0.7] }.eval(0.3), 0.7);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn monotone_for_increasing_data(ys in proptest::collection::vec(0.0f64..1.0, 3..8)) {
|
||||
|
||||
@@ -31,7 +31,7 @@ impl DateTime {
|
||||
// chrono rejects impossible days (leap years and all); the time is checked
|
||||
// by hand so a leap second (`second == 60`) stays acceptable.
|
||||
let day_ok = self.year > 0 && chrono::NaiveDate::from_ymd_opt(i32::from(self.year), u32::from(self.month), u32::from(self.day)).is_some();
|
||||
let ok = day_ok && self.hour < 24 && self.minute < 60 && self.second < 61 && self.offset_minutes.is_none_or(|o| o.abs() <= 18 * 60);
|
||||
let ok = day_ok && self.hour < 24 && self.minute < 60 && self.second < 61 && self.offset_minutes.is_none_or(|o| o.unsigned_abs() <= 18 * 60);
|
||||
ok.then_some(self)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,8 @@ impl DateTime {
|
||||
}
|
||||
if let Some(o) = self.offset_minutes {
|
||||
let sign = if o < 0 { '-' } else { '+' };
|
||||
s += &format!("{sign}{:02}:{:02}", o.abs() / 60, o.abs() % 60);
|
||||
let o = o.unsigned_abs();
|
||||
s += &format!("{sign}{:02}:{:02}", o / 60, o % 60);
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -189,6 +190,14 @@ mod tests {
|
||||
assert_eq!(DateTime::parse_offset("+99:00"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extreme_offsets_do_not_overflow() {
|
||||
// `DateTime` deserializes with public fields, so an offset can arrive without passing `valid()`
|
||||
let d = DateTime { offset_minutes: Some(i16::MIN), ..DateTime::parse_iso("2024-06-01T13:45:59").unwrap() };
|
||||
assert_eq!(d.to_iso(), "2024-06-01T13:45:59-546:08");
|
||||
assert_eq!(d.valid(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_offsets_are_rejected_without_panicking() {
|
||||
for offset in ["+1€", "-1€", "+€1", "-€1"] {
|
||||
|
||||
Reference in New Issue
Block a user