Rename chuva::Model to chuva::Chuva 💬 by Caio 7 months ago (log)
Consumed my persim berry
Consumed my persim berry
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
use std::path::Path;
use fst::{Automaton, IntoStreamer, Streamer};
use jiff::Timestamp;
use chuva::{MAX_OFFSET, Model, Projector, STEPS};
type Result<T> = crate::Result<T>;
pub type Prediction<'a> = &'a [f32; STEPS];
pub struct Moros {
model: Model,
proj: Projector,
fst: fst::Map<&'static [u8]>,
}
impl Moros {
pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
let model = Model::load_from_dir(dir)?;
let fst = fst::Map::new(FST_STATE)?;
Ok(Self {
proj: Projector::new(),
fst,
model,
})
}
pub fn by_lat_lon(&self, lat: f64, lon: f64) -> Option<Prediction<'_>> {
let offset = self.proj.to_offset(lat, lon)?;
self.by_offset(offset)
}
pub fn by_postcode(&self, code: &str) -> Option<Prediction<'_>> {
let mut stream = self
.fst
.search(AsciiUpperCase::new(code).starts_with())
.into_stream();
let (_, offset) = stream.next()?;
self.by_offset(offset as usize)
}
pub fn by_postcode4(&self, code: &str) -> Option<Prediction<'_>> {
let mut stream = self.fst.range().gt(code).into_stream();
let (key, offset) = stream.next()?;
assert_eq!(6, key.len(), "key is pc6");
if &key[..4] == code.as_bytes() {
self.by_offset(offset as usize)
} else {
None
}
}
#[inline]
pub(crate) fn by_offset(&self, offset: usize) -> Option<Prediction<'_>> {
assert!(offset <= MAX_OFFSET);
Some(
self.model.data[offset..(offset + STEPS)]
.try_into()
.unwrap(),
)
}
pub fn created_at(&self) -> Timestamp {
self.model.created_at
}
pub fn filename(&self) -> &str {
&self.model.filename
}
pub fn kind(&self) -> chuva::ModelKind {
self.model.kind
}
pub fn get_time_slot(&self, now: Timestamp) -> Result<usize> {
get_time_slot(self.model.created_at, now).map_err(|_| "Dataset too old".into())
}
}
static FST_STATE: &[u8] = include_bytes!("../asset/postcodes.fst").as_slice();
fn get_time_slot(created_at: Timestamp, now: Timestamp) -> std::result::Result<usize, i64> {
let age = (now - created_at)
.total(jiff::Unit::Minute)
.map_err(|_| 420)?; // irrelevant
if !(0.0..=120.0).contains(&age) {
Err(age as i64)
} else {
let slot = (age / 5.0) as usize;
assert!(slot < STEPS);
Ok(slot)
}
}
#[cfg(test)]
mod tests {
use super::{AsciiUpperCase, FST_STATE, get_time_slot};
use fst::{Automaton, IntoStreamer, Streamer};
use jiff::{Timestamp, ToSpan};
#[test]
fn slot_works() {
let now = Timestamp::now();
assert_eq!(Err(-1), get_time_slot(now, now - 1.minute()));
assert_eq!(Ok(0), get_time_slot(now, now));
assert_eq!(Ok(24), get_time_slot(now, now + 2.hours()));
assert_eq!(Err(121), get_time_slot(now, now + 121.minutes()));
}
#[test]
fn case_insensitive_postcode_search() {
let fst = fst::Map::new(FST_STATE).expect("valid fst state");
let _ = fst.get("1017CE").expect("Key 1017CE exists in the fst");
let mut stream = fst
.search(AsciiUpperCase::new("1017ce").starts_with())
.into_stream();
let (key, _) = stream.next().expect("lower case search matches");
assert_eq!(
b"1017CE", key,
"lower case search should match upper case key"
);
}
}
|