From 2ac47434a65cf6ccfdcc9b7a6c84ccfe478855fd Mon Sep 17 00:00:00 2001 From: L <457124+liborty@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:29:55 +1000 Subject: [PATCH] 3.0.5 --- Cargo.toml | 3 +- README.md | 2 + src/algos.rs | 179 +-------------------------------------------- src/algosf64.rs | 190 ------------------------------------------------ src/lib.rs | 26 ++----- src/oldalgos.rs | 170 +++++++++++++++++++++++++++++++++++++++++++ tests/tests.rs | 19 ++--- 7 files changed, 193 insertions(+), 396 deletions(-) delete mode 100644 src/algosf64.rs diff --git a/Cargo.toml b/Cargo.toml index 1a690c5..9004244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "medians" -version = "3.0.4" +version = "3.0.5" authors = ["Libor Spacek"] edition = "2021" description = "Median, Statistical Measures, Mathematics, Statistics" @@ -13,7 +13,6 @@ categories = ["science","mathematics","algorithms"] include = [ "src/lib.rs", "src/algos.rs", - "src/algosf64.rs", "src/error.rs", "Cargo.toml", "README.md", diff --git a/README.md b/README.md index 671bc7d..58af5ab 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ pub trait Median<'a, T> { ## Release Notes +**Version 3.0.5** - Obsolete code pruning. + **Version 3.0.4** - Some minor code simplifications. **Version 3.0.3** - Updated dev dependency `ran` to 2.0. diff --git a/src/algos.rs b/src/algos.rs index 4584947..9c84e69 100644 --- a/src/algos.rs +++ b/src/algos.rs @@ -1,128 +1,11 @@ -use crate::Me; use core::cmp::{Ordering, Ordering::*}; use std::ops::Range; -const FSIGN: u64 = 0x8000_0000_0000_0000; - -/// Scan a slice of f64s for being free of NANs -pub fn no_nans(v: &[f64]) -> bool { - for &f in v { - if f.is_nan() { - return false; - }; - } - true -} - -/// Copies a slice of f64s, removing any NANs from it. -/// It is advisable to test with `non_nans` first, as there may be none -pub fn scrub_nans(v: &[f64]) -> Vec { - v.iter() - .filter_map(|&f| if f.is_nan() { None } else { Some(f) }) - .collect::>() - // Err(merror("nan", "median_checked: NaN encountered in data")) -} - -/// Converts f64s to &u64s, so that sort order is preserved, including NANs and all -pub fn to_u64s(v: &[f64]) -> Result, Me> { - v.iter() - .map(|f| { - let mut u: u64 = f.to_bits(); - if (u >> 63) == 1 { - u ^= FSIGN; - } else { - u = !u; - }; - Ok(u) - }) - .collect() -} - -/// Converts f64s to &u64s (inverse of `to_u64s`. -pub fn to_f64s(v: &[u64]) -> Vec { - v.iter() - .map(|&u| { - if (u >> 63) == 1 { - f64::from_bits(!u) - } else { - f64::from_bits(u ^ FSIGN) - } - }) - .collect() -} - /// Constructs ref wrapped `Vec<&T>` from `&[T] in rng` pub fn ref_vec(v: &[T], rng: Range) -> Vec<&T> { v.iter().take(rng.end).skip(rng.start).collect() } -/// Builds Vec from refs in Vec<&T> (inverse of ref_vec()) -pub fn deref_vec(v: &[&T]) -> Vec -where - T: Clone, -{ - v.iter().map(|&x| x.clone()).collect() -} - -/// middle valued ref out of three, at most three comparisons -pub fn midof3<'a, T>( - item1: &'a T, - item2: &'a T, - item3: &'a T, - c: &mut impl FnMut(&T, &T) -> Ordering, -) -> &'a T { - let (min, max) = if c(item2, item1) == Less { - (item2, item1) - } else { - (item1, item2) - }; - if c(min, item3) != Less { - return min; - }; - if c(item3, max) != Less { - return max; - }; - item3 -} - -/* -/// pivot estimate as recursive mid of mids of three -pub fn midofmids<'a, T>(s: &[&T], rng: Range, c: &mut impl FnMut(&T, &T) -> Ordering) -> &'a T -where - T: PartialOrd, -{ - if s.len() < 3 { return s[0]; }; - for i in - let (min, max) = if *item1 <= *item2 { - (item1, item2) - } else { - (item2, item1) - }; - if *item3 <= *min { - return min; - }; - if *item3 <= *max { - return item3; - }; - max -} - -/// Mid two values of four refs. Makes four comparisons -fn mid2( - idx0: usize, - idx1: usize, - idx2: usize, - idx3: usize, - c: &mut impl FnMut(usize, usize) -> Ordering, -) -> (usize,usize) { - let (min1,max1) = if c(idx0, idx1) == Less { (idx0,idx1) } else { (idx1,idx0) }; - let (min2,max2) = if c(idx2, idx3) == Less { (idx2,idx3) } else { (idx3,idx2) }; - let mid1 = if c(min1, min2) == Less { min2 } else { min1 }; - let mid2 = if c(max1, max2) == Less { max1 } else { max2 }; - (mid1,mid2) -} -*/ - /// Index of the middling value of four refs. Makes only three comparisons fn middling( idx0: usize, @@ -176,13 +59,6 @@ pub fn min2<'a, T>( (min1, min2) } -/* -/// Maps general `quantify` closure to self, converting the type T -> U -pub fn quant_vec(v: &[T], quantify: impl Fn(&T) -> U) -> Vec { - v.iter().map(quantify).collect::>() -} -*/ - /// measure errors from centre (for testing) /// requires quantising to f64 for accuracy pub fn qbalance(s: &[T], centre: &f64, q: impl Fn(&T) -> f64) -> i64 { @@ -206,9 +82,9 @@ pub fn qbalance(s: &[T], centre: &f64, q: impl Fn(&T) -> f64) -> i64 { /// Partitions `s: &mut [&T]` within range `rng`, using comparator `c`. /// The first item `s[rng.start]` is assumed to be the pivot. -/// The three rearranged partitions are by eqstart,gtstart, where: +/// The three rearranged partitions are demarcated by eqstart,gtstart, where: /// `rng.start..eqstart` (may be empty) contains refs to items lesser than the pivot, -/// `gtstart-eqstart` is the number (>= 1) of items equal to the pivot, values within this subrange are undefined, +/// `gtstart-eqstart` is the number (>= 1) of items equal to the pivot (values within this subrange are undefined) /// `gtstart..rng.end` (may be empty) contains refs to items greater than the pivot. pub fn part( s: &mut [&T], @@ -290,58 +166,7 @@ pub fn evenmedianu8(s: &[u8]) -> f64 { res } -/// Odd median of `&[u16]` -pub fn oddmedianu16(s: &[u16]) -> f64 { - let need = s.len() / 2; // median target position - let mut histogram = [0_usize; 65536]; - let mut cummulator = 0_usize; - let mut res = 0_f64; - for &u in s.iter() { - histogram[u as usize] += 1; - } - for (i, &hist) in histogram.iter().enumerate() { - if hist == 0 { - continue; - }; - cummulator += hist; - if need < cummulator { - res = i as f64; - break; - }; - } - res -} -/// Even median of `&[u16]` -pub fn evenmedianu16(s: &[u16]) -> f64 { - let need = s.len() / 2; // first median target position - let mut histogram = [0_usize; 65536]; - let mut cummulator = 0_usize; - let mut firstres = true; - let mut res = f64::MIN; - for &u in s.iter() { - histogram[u as usize] += 1; - } - for (i, &hist) in histogram.iter().enumerate() { - if hist == 0 { - continue; - }; - cummulator += hist; - if firstres { - if need < cummulator { res = i as f64; break; }; // cummulator exceeds need, found both items - if need == cummulator { // found first item (last in this bucket) - res = i as f64; - firstres = false; - continue; // search for the second item - }; - } else { // the second item is in the first following non-zero bucket - res += i as f64; - res /= 2.0; - break; - }; // found the second - }; - res - } /// Median of odd sized generic data with Odering comparisons by custom closure pub fn oddmedian_by<'a, T>(s: &mut [&'a T], c: &mut impl FnMut(&T, &T) -> Ordering) -> &'a T { let mut rng = 0..s.len(); diff --git a/src/algosf64.rs b/src/algosf64.rs deleted file mode 100644 index 2e78592..0000000 --- a/src/algosf64.rs +++ /dev/null @@ -1,190 +0,0 @@ -use core::ops::Range; - -/// finds a mid value of sample of three -pub fn midof3(item1: T, item2: T, item3: T) -> T { - let (min, max) = if item1 <= item2 { - (item1, item2) - } else { - (item2, item1) - }; - if item3 <= min { - return min; - }; - if item3 <= max { - return item3; - }; - max -} - -/// Reorders (partitions by pivot) mutable set `s` within subrange `rng`. -/// Returns `(gtsub,endsub)` after a single efficient pass. -/// All items in rng.start..gtsub will be less than the pivot. -/// All items in gtsub..endsub will be greater than the pivot. -/// Items in endsub..rng.end will have arbitrary values but the length -/// of this subrange is the number of items equal to the pivot. -pub fn part(s: &mut [T], pivot:T, rng: &Range) -> (usize, usize) { - let mut ltsub = rng.start; - let mut endsub = rng.end; - let mut gtsub = endsub-1; - - loop { - if gtsub < ltsub { return (ltsub,endsub); }; - if s[gtsub] > pivot { - gtsub -= 1; - continue; - }; - if s[gtsub] == pivot { - endsub -= 1; - s[gtsub] = s[endsub]; // replace eq item with gt item - gtsub -= 1; - continue; - }; - // now s[gtsub] < pivot - 'ltloop: loop { - if ltsub == gtsub { return (ltsub+1,endsub); }; - if s[ltsub] < pivot { - ltsub += 1; - continue 'ltloop; - }; - if s[ltsub] == pivot { - s[ltsub] = s[gtsub]; // s[gtsub] < pivot, so copy it to the lt set - endsub -= 1; - s[gtsub] = s[endsub]; // shift gt set one down into the vacated space - } else { - s.swap(ltsub, gtsub); - }; - // now s[ltsub] < pivot < s[gtsub] - ltsub += 1; - if ltsub == gtsub { - return (gtsub,endsub); // ltsub and gtsub are adjacent and swapped - }; - gtsub -= 1; - }; - }; -} - -/// Minimum value within a range in a slice -pub fn min(s: &[T], rng: Range) -> T { - let mut min = s[rng.start]; - for &si in s.iter().take(rng.end).skip(rng.start + 1) { - if si < min { - min = si; - }; - } - min -} - -/// Maximum value within a range in a slice -pub fn max(s: &[T], rng: Range) -> T { - let mut max = s[rng.start]; - for &si in s.iter().take(rng.end).skip(rng.start + 1) { - if si > max { - max = si; - }; - } - max -} - -/// two minimum values, in order -pub fn min2(s: &[T], rng: Range) -> (T, T) { - let (mut min1, mut min2) = if s[rng.start + 1] < s[rng.start] { - (s[rng.start + 1], s[rng.start]) - } else { - (s[rng.start], s[rng.start + 1]) - }; - for &si in s.iter().take(rng.end).skip(rng.start + 2) { - if si < min1 { - min2 = min1; - min1 = si; - } else if si < min2 { - min2 = si; - } - } - (min1, min2) -} - -/// two maximum values, in order -pub fn max2(s: &[T], rng: Range) -> (T, T) { - let (mut max1, mut max2) = if s[rng.start + 1] > s[rng.start] { - (s[rng.start + 1], s[rng.start]) - } else { - (s[rng.start], s[rng.start + 1]) - }; - for &si in s.iter().take(rng.end).skip(rng.start + 2) { - if si > max1 { - max2 = max1; - max1 = si; - } else if si > max2 { - max2 = si; - } - } - (max2, max1) -} - -/// Median of slice s of odd length -pub fn med_odd(s: &mut [T]) -> T { - let mut rng = 0..s.len(); - let mut need = s.len() / 2; // need as subscript - loop { - // todo: test here for rng.len() == 3 - // Takes three samples of data and uses their midpoint as a pivot - let pivot = midof3(s[rng.start], s[(rng.start+rng.end)/2], s[rng.end-1]); - let (gtsub, endsub) = part(s, pivot, &rng); - // we are somewhere within ltset, iterate on it - if need + 2 < gtsub { - rng.end = gtsub; - continue; - }; - // close to the end of ltset, we have a solution: - if need + 2 == gtsub { return max2(s, rng.start..gtsub).0; }; - if need + 1 == gtsub { return max(s, rng.start..gtsub); }; - - // jump over equals set - need -= rng.end - endsub; - if need < gtsub { return pivot; }; // in equals set - - // somewhere within gtset, iterate on it - if need > gtsub + 1 { - rng.start = gtsub; - rng.end = endsub; - continue; - }; - // at the beginning of the gt set - if need == gtsub { return min(s, gtsub..endsub); }; - // else must be second in the gt set - return min2(s, gtsub..endsub).1; - }; -} - -/// Both central values of s of even length -pub fn med_even(s: &mut [T]) -> (T, T) { - let mut rng = 0..s.len(); - let mut need = s.len() / 2 - 1; // need as subscript - 1 - loop { - let pivot = midof3(s[rng.start], s[(rng.start+rng.end)/2], s[rng.end-1]); - let (gtsub, endsub) = part(s, pivot, &rng); - // we are somewhere within ltset, iterate on it - if need + 2 < gtsub { - rng.end = gtsub; - continue; - }; - // need is close to the end of ltset, we have a solution - if need + 2 == gtsub { return max2(s, rng.start..gtsub); }; - // there will always be at least one item equal to pivot and therefore it is the minimum of the ge set - if need + 1 == gtsub { return ( max(s, rng.start..gtsub), pivot ); }; - - // jump over equals set - need -= rng.end - endsub; - if need + 1 < gtsub { return (pivot,pivot); }; // in equals set - if need + 1 == gtsub { return ( pivot, min(s, gtsub..endsub)); }; // last of equals set - - // somewhere within gt set, iterate on it - if need > gtsub + 1 { - rng.start = gtsub; - rng.end = endsub; - continue; - }; - // at the beginning of the gt set - if need == gtsub { return min2(s, gtsub..endsub); }; - } -} diff --git a/src/lib.rs b/src/lib.rs index 30ba27e..4928e8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,26 +59,11 @@ pub fn medianu8(s:&[u8]) -> Result { } } -/// Median of primitive type u16 by fast radix search -pub fn medianu16(s:&[u16]) -> Result { - let n = s.len(); - match n { - 0 => return merror("size", "median: zero length data"), - 1 => return Ok(s[0] as f64), - 2 => return Ok((s[0] as f64 + s[1] as f64) / 2.0), - _ => (), - }; - if (n & 1) == 1 { - Ok(oddmedianu16(s)) - } else { - Ok(evenmedianu16(s)) - } -} /// Fast 1D medians of floating point data, plus related methods pub trait Medianf64 { - /// Median of f64s, checked for NaNs + /// Median of f64s, NaNs removed fn medf_checked(self) -> Result; - /// Median of f64s, not checked for NaNs + /// Median of f64s, including NaNs fn medf_unchecked(self) -> f64; /// Zero mean/median data produced by subtracting the centre fn medf_zeroed(self, centre: f64) -> Vec; @@ -122,6 +107,11 @@ impl Medianf64 for &[f64] { 2 => return Ok((self[0] + self[1]) / 2.0), _ => (), }; + /* + if !no_nans(self) { return merror("Nan", "medf_checked: Nan found in input!");}; + let us = &to_u64s(self); + let mut s = ref_vec(us,0..self.len()); + */ let mut s = self .iter() .map(|x| { @@ -137,7 +127,7 @@ impl Medianf64 for &[f64] { Ok(*oddm) } else { let (&med1, &med2) = evenmedian_by(&mut s, &mut ::total_cmp); - Ok((med1 + med2) / 2.0) + Ok((med1+med2) / 2.0) } } /// Use this when your data does not contain any NaNs. diff --git a/src/oldalgos.rs b/src/oldalgos.rs index fbf58a6..128b1f9 100644 --- a/src/oldalgos.rs +++ b/src/oldalgos.rs @@ -2,6 +2,176 @@ use core::ops::Range; use core::fmt::Debug; use core::ops::{Deref, Neg}; +const FSIGN: u64 = 0x8000_0000_0000_0000; + +/// Scan a slice of f64s for being free of NANs +pub fn no_nans(v: &[f64]) -> bool { + for &f in v { + if f.is_nan() { + return false; + }; + } + true +} + +/// Copies a slice of f64s, removing any NANs from it. +/// It is advisable to test with `non_nans` first, as there may be none +pub fn scrub_nans(v: &[f64]) -> Vec { + v.iter() + .filter_map(|&f| if f.is_nan() { None } else { Some(f) }) + .collect::>() +} + +/// Converts one f64, including NaNs etc., to u64, maintaining order +pub fn to_u64(f: f64) -> u64 { + let u: u64 = f.to_bits(); + if (u >> 63) == 1 { u^FSIGN } else { !u } +} + +/// Converts slice of f64s, including NaNs etc., to Vec<&u64>, maintaining order +pub fn to_u64s(v: &[f64]) -> Vec { + v.iter().map(|&f| to_u64(f)).collect() +} + +/// Converts slice of f64s to u64s, so that sort order is preserved, cleaning NANs +pub fn to_clean_u64s(v: &[f64]) -> Vec { + v.iter() + .filter_map(|&f| if f.is_nan() { None } else { Some(to_u64(f)) } ) + .collect() +} + +/// Converts f64 to u64 (inverse of `to_u64`). +pub fn to_f64(u: u64) -> f64 { + f64::from_bits( if (u >> 63) == 1 { !u } else { u^FSIGN } ) +} + +/// Converts u64s to f64s (inverse of `to_u64s`). +pub fn to_f64s(v: &[u64]) -> Vec { + v.iter().map(|&u| to_f64(u)).collect() +} + +/// Builds Vec from refs in Vec<&T> (inverse of ref_vec()) +pub fn deref_vec(v: &[&T]) -> Vec +where + T: Clone, +{ + v.iter().map(|&x| x.clone()).collect() +} + +/// Maps general `quantify` closure to self, converting the type T -> U +pub fn quant_vec(v: &[T], quantify: impl Fn(&T) -> U) -> Vec { + v.iter().map(quantify).collect::>() +} + +/// middle valued ref out of three, at most three comparisons +pub fn midof3<'a, T>( + item1: &'a T, + item2: &'a T, + item3: &'a T, + c: &mut impl FnMut(&T, &T) -> Ordering, +) -> &'a T { + let (min, max) = if c(item2, item1) == Less { + (item2, item1) + } else { + (item1, item2) + }; + if c(min, item3) != Less { + return min; + }; + if c(item3, max) != Less { + return max; + }; + item3 +} + +/// pivot estimate as recursive mid of mids of three +pub fn midofmids<'a, T>(s: &[&T], rng: Range, c: &mut impl FnMut(&T, &T) -> Ordering) -> &'a T +where + T: PartialOrd, +{ + if s.len() < 3 { return s[0]; }; + let (min, max) = if *item1 <= *item2 { + (item1, item2) + } else { + (item2, item1) + }; + if *item3 <= *min { + return min; + }; + if *item3 <= *max { + return item3; + }; + max +} + +/// Mid two values of four refs. Makes four comparisons +fn mid2( + idx0: usize, + idx1: usize, + idx2: usize, + idx3: usize, + c: &mut impl FnMut(usize, usize) -> Ordering, +) -> (usize,usize) { + let (min1,max1) = if c(idx0, idx1) == Less { (idx0,idx1) } else { (idx1,idx0) }; + let (min2,max2) = if c(idx2, idx3) == Less { (idx2,idx3) } else { (idx3,idx2) }; + let mid1 = if c(min1, min2) == Less { min2 } else { min1 }; + let mid2 = if c(max1, max2) == Less { max1 } else { max2 }; + (mid1,mid2) +} + +/// Odd median of `&[u16]` +pub fn oddmedianu16(s: &[u16]) -> f64 { + let need = s.len() / 2; // median target position + let mut histogram = [0_usize; 65536]; + let mut cummulator = 0_usize; + let mut res = 0_f64; + for &u in s.iter() { + histogram[u as usize] += 1; + } + for (i, &hist) in histogram.iter().enumerate() { + if hist == 0 { + continue; + }; + cummulator += hist; + if need < cummulator { + res = i as f64; + break; + }; + } + res +} + +/// Even median of `&[u16]` +pub fn evenmedianu16(s: &[u16]) -> f64 { + let need = s.len() / 2; // first median target position + let mut histogram = [0_usize; 65536]; + let mut cummulator = 0_usize; + let mut firstres = true; + let mut res = f64::MIN; + for &u in s.iter() { + histogram[u as usize] += 1; + } + for (i, &hist) in histogram.iter().enumerate() { + if hist == 0 { + continue; + }; + cummulator += hist; + if firstres { + if need < cummulator { res = i as f64; break; }; // cummulator exceeds need, found both items + if need == cummulator { // found first item (last in this bucket) + res = i as f64; + firstres = false; + continue; // search for the second item + }; + } else { // the second item is in the first following non-zero bucket + res += i as f64; + res /= 2.0; + break; + }; // found the second + }; + res + } + /// The following defines Ord struct which is a T that implements Ord. /// This boilerplate makes any wrapped T:PartialOrd, such as f64, into Ord #[derive(Clone,Copy,Debug)] diff --git a/tests/tests.rs b/tests/tests.rs index 47fad68..4156da9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] #[cfg(test)] use indxvec::{here, printing::*, Indices, Mutops, Printing, Vecops}; -use medians::algos::{scrub_nans, to_u64s, to_f64s, qbalance, part, ref_vec, deref_vec, min, min2}; +use medians::algos::{qbalance, part, ref_vec, min, min2}; // use medians::algosf64::partord; use medians::{Me, merror, medianu8, Median, Medianf64}; use ran:: *; @@ -17,9 +17,9 @@ fn parting() -> Result<(), Me> { let data = [ 5., 8., 7., 6., 5., 4., 3., 2., -f64::NAN, 1., 0., 1., -2., 3., 4., -5., f64::NAN, f64::NAN, 6., 7., 7., ]; - println!("To u64s: {}",to_u64s(&data)?.gr()); - println!("To f64s: {}",to_f64s(&to_u64s(&data)?).gr()); - println!("Scrubbed: {}", scrub_nans(&to_f64s(&to_u64s(&data)?)).gr()); + // println!("To u64s: {}",to_u64s(&data).gr()); + // println!("To f64s: {}",to_f64s(&to_u64s(&data)).gr()); + // println!("Scrubbed: {}", scrub_nans(&to_f64s(&to_u64s(&data))).gr()); let len = data.len(); println!("Pivot {}: {}", data[0].yl(), data.gr()); let mut refdata = ref_vec(&data, 0..len); @@ -39,7 +39,7 @@ fn parting() -> Result<(), Me> { data[0].yl() ); let refindex = data.isort_refs(0..len, |a,b| a.total_cmp(b)); - println!("isort_refs ascending sorted:\n{}",deref_vec(&refindex).gr()); + println!("isort_refs ascending sorted:\n{}",&refindex.gr()); let indx = data.isort_indexed(0..len, |a,b| b.total_cmp(a)); println!("isort_index (descending):\n{}",indx.gr()); println!("Unindexed:\n{}",indx.unindex(&data,true).gr()); @@ -69,7 +69,7 @@ fn text() { } #[test] -fn medf64() { +fn medf64() -> Result<(), Me> { let v = [ 9., 10., 18., 17., 16., 15., 14., 1., 2., 3., 4., 5., 6., 7., 8., 17., 10., 11., 12., 13., 14., 15., 16., 18., 9. ]; @@ -93,9 +93,10 @@ fn medf64() { (gtsub - eqsub).yl(), v[0].yl() ); - let median = v.medf_unchecked(); + let median = v.medf_checked()?; let mad = v.madf(median); println!("\nMedian±mad: {GR}{}±{}{UN}", median, mad); + Ok(()) } #[test] @@ -144,7 +145,7 @@ const NAMES: [&str; 3] = [ // "medf_unchecked", "qmedian", "median_by", - "medianu8" + "medianu8" ]; const CLOSURESU8: [fn(&[u8]); 3] = [ @@ -166,7 +167,7 @@ const CLOSURESU8: [fn(&[u8]); 3] = [ |v: &[_]| { medianu8(v) .expect("medianu8 closure failed"); - }, + } ]; #[test]