Merge remote-tracking branch 'origin/main'

This commit is contained in:
boris
2026-09-13 11:06:22 +08:00
3 changed files with 86 additions and 39 deletions
+7 -9
View File
@@ -4390,9 +4390,9 @@ fn normalize_factor_snapshots(
});
}
let already_normalized = snapshot.extra_factors.iter().all(|(field, value)| {
let trimmed = field.as_ref().trim().trim_matches('"').trim_matches('\'');
let trimmed = field.as_str().trim().trim_matches('"').trim_matches('\'');
!trimmed.is_empty()
&& trimmed == field.as_ref()
&& trimmed == field.as_str()
&& trimmed.bytes().all(|byte| !byte.is_ascii_uppercase())
&& value.is_finite()
});
@@ -4403,15 +4403,15 @@ fn normalize_factor_snapshots(
.extra_factors
.into_iter()
.filter_map(|(field, value)| {
let trimmed = field.as_ref().trim().trim_matches('"').trim_matches('\'');
let trimmed = field.as_str().trim().trim_matches('"').trim_matches('\'');
if trimmed.is_empty() || !value.is_finite() {
None
} else if trimmed == field.as_ref()
} else if trimmed == field.as_str()
&& trimmed.bytes().all(|byte| !byte.is_ascii_uppercase())
{
Some((field, value))
} else {
Some((Cow::Owned(trimmed.to_ascii_lowercase()), value))
Some((CompactString::from(trimmed.to_ascii_lowercase()), value))
}
})
.collect();
@@ -6470,10 +6470,8 @@ mod tests {
extra_factors: From::from([(Cow::Borrowed("amount"), 10.0)]),
}])
.expect("normalize clean factor snapshot");
assert!(matches!(
clean[0].extra_factors.keys().next(),
Some(Cow::Borrowed("amount"))
));
assert_eq!(clean[0].extra_factors.keys().next().map(CompactString::as_str), Some("amount"));
assert!(!clean[0].extra_factors.keys().next().unwrap().is_heap_allocated());
let dirty = normalize_factor_snapshots(vec![DailyFactorSnapshot {
date,
+76 -27
View File
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::ops::Index;
use compact_str::CompactString;
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -10,7 +11,14 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Sorted numeric fields stored contiguously, without a tree node per snapshot.
#[derive(Clone, Default, PartialEq)]
pub struct NumericFactorMap {
entries: Vec<(Cow<'static, str>, f64)>,
entries: Vec<(CompactString, f64)>,
}
fn compact_key(key: Cow<'static, str>) -> CompactString {
match key {
Cow::Borrowed(value) => CompactString::const_new(value),
Cow::Owned(value) => CompactString::from(value),
}
}
impl NumericFactorMap {
@@ -32,14 +40,14 @@ impl NumericFactorMap {
pub fn get(&self, key: &str) -> Option<&f64> {
self.entries
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| &self.entries[index].1)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut f64> {
self.entries
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| &mut self.entries[index].1)
}
@@ -49,17 +57,21 @@ impl NumericFactorMap {
}
pub fn insert(&mut self, key: Cow<'static, str>, value: f64) -> Option<f64> {
self.insert_compact(compact_key(key), value)
}
pub fn insert_compact(&mut self, key: CompactString, value: f64) -> Option<f64> {
if self
.entries
.last()
.is_none_or(|(last, _)| last.as_ref() < key.as_ref())
.is_none_or(|(last, _)| last.as_str() < key.as_str())
{
self.entries.push((key, value));
return None;
}
match self
.entries
.binary_search_by(|(name, _)| name.as_ref().cmp(key.as_ref()))
.binary_search_by(|(name, _)| name.as_str().cmp(key.as_str()))
{
Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
Err(index) => {
@@ -71,19 +83,19 @@ impl NumericFactorMap {
pub fn remove(&mut self, key: &str) -> Option<f64> {
self.entries
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
.binary_search_by(|(name, _)| name.as_str().cmp(key))
.ok()
.map(|index| self.entries.remove(index).1)
}
pub fn retain(&mut self, mut keep: impl FnMut(&Cow<'static, str>, &mut f64) -> bool) {
pub fn retain(&mut self, mut keep: impl FnMut(&CompactString, &mut f64) -> bool) {
self.entries.retain_mut(|(key, value)| keep(key, value));
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.entries.iter())
}
pub fn keys(&self) -> impl DoubleEndedIterator<Item = &Cow<'static, str>> + ExactSizeIterator {
pub fn keys(&self) -> impl DoubleEndedIterator<Item = &CompactString> + ExactSizeIterator {
self.entries.iter().map(|(key, _)| key)
}
pub fn values(&self) -> impl DoubleEndedIterator<Item = &f64> + ExactSizeIterator {
@@ -104,9 +116,9 @@ impl Index<&str> for NumericFactorMap {
}
}
pub struct Iter<'a>(std::slice::Iter<'a, (Cow<'static, str>, f64)>);
pub struct Iter<'a>(std::slice::Iter<'a, (CompactString, f64)>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a Cow<'static, str>, &'a f64);
type Item = (&'a CompactString, &'a f64);
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|(k, v)| (k, v))
}
@@ -121,14 +133,14 @@ impl DoubleEndedIterator for Iter<'_> {
}
impl ExactSizeIterator for Iter<'_> {}
impl<'a> IntoIterator for &'a NumericFactorMap {
type Item = (&'a Cow<'static, str>, &'a f64);
type Item = (&'a CompactString, &'a f64);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl IntoIterator for NumericFactorMap {
type Item = (Cow<'static, str>, f64);
type Item = (CompactString, f64);
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
@@ -137,6 +149,11 @@ impl IntoIterator for NumericFactorMap {
impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap {
fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(iter: T) -> Self {
iter.into_iter().map(|(key, value)| (compact_key(key), value)).collect()
}
}
impl FromIterator<(CompactString, f64)> for NumericFactorMap {
fn from_iter<T: IntoIterator<Item = (CompactString, f64)>>(iter: T) -> Self {
let mut entries: Vec<_> = iter.into_iter().collect();
// Stable sorting preserves last-value-wins for repeated input keys.
if !entries.windows(2).all(|pair| pair[0].0 <= pair[1].0) {
@@ -155,6 +172,11 @@ impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap {
}
impl Extend<(Cow<'static, str>, f64)> for NumericFactorMap {
fn extend<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(&mut self, iter: T) {
self.extend(iter.into_iter().map(|(key, value)| (compact_key(key), value)));
}
}
impl Extend<(CompactString, f64)> for NumericFactorMap {
fn extend<T: IntoIterator<Item = (CompactString, f64)>>(&mut self, iter: T) {
let mut incoming: Self = iter.into_iter().collect();
if incoming.is_empty() {
return;
@@ -194,9 +216,7 @@ impl<const N: usize> From<[(Cow<'static, str>, f64); N]> for NumericFactorMap {
}
impl From<BTreeMap<Cow<'static, str>, f64>> for NumericFactorMap {
fn from(entries: BTreeMap<Cow<'static, str>, f64>) -> Self {
Self {
entries: entries.into_iter().collect(),
}
entries.into_iter().collect()
}
}
@@ -219,8 +239,8 @@ impl<'de> Deserialize<'de> for NumericFactorMap {
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry::<String, f64>()? {
entries.push((Cow::Owned(key), value));
while let Some((key, value)) = map.next_entry::<CompactString, f64>()? {
entries.push((key, value));
}
Ok(entries.into_iter().collect())
}
@@ -233,6 +253,34 @@ impl<'de> Deserialize<'de> for NumericFactorMap {
mod tests {
use super::*;
#[test]
fn compact_keys_inline_dynamic_names_and_keep_long_static_storage() {
const LONG: &str = "a_long_static_factor_identifier_that_must_remain_borrowed";
let map = NumericFactorMap::from([
(Cow::Owned("dynamic_factor_20".to_owned()), -0.0),
(Cow::Borrowed(LONG), 1.0),
]);
let cloned = map.clone();
let short = cloned.keys().find(|key| key.as_str() == "dynamic_factor_20").unwrap();
assert!(!short.is_heap_allocated());
let long = cloned.keys().find(|key| key.as_str() == LONG).unwrap();
assert_eq!(long.as_static_str(), Some(LONG));
assert_eq!(cloned["dynamic_factor_20"].to_bits(), (-0.0_f64).to_bits());
assert_eq!(std::mem::size_of::<(CompactString, f64)>(), std::mem::size_of::<(Cow<'static, str>, f64)>());
}
#[test]
fn long_dynamic_unicode_and_short_keys_keep_the_same_json_map() {
let entries = ["", "a", "a_field_longer_than_the_inline_string_capacity", "价格因子", "ths_up_days_stock"]
.into_iter().enumerate().map(|(index, key)| (Cow::Owned(key.to_string()), index as f64 + 0.25))
.collect::<BTreeMap<_, _>>();
let map = NumericFactorMap::from(entries.clone());
assert_eq!(serde_json::to_string(&map).unwrap(), serde_json::to_string(&entries).unwrap());
let decoded: NumericFactorMap = serde_json::from_str(&serde_json::to_string(&map).unwrap()).unwrap();
assert_eq!(decoded, map);
assert!(!decoded.keys().find(|key| key.as_str() == "ths_up_days_stock").unwrap().is_heap_allocated());
}
#[test]
fn updates_order_removal_and_values_match_tree_map() {
let mut flat = NumericFactorMap::new();
@@ -249,14 +297,14 @@ mod tests {
}
}
assert_eq!(
flat.iter().collect::<Vec<_>>(),
tree.iter().collect::<Vec<_>>()
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
flat.retain(|_, value| *value > 100.0);
tree.retain(|_, value| *value > 100.0);
assert_eq!(
flat.iter().collect::<Vec<_>>(),
tree.iter().collect::<Vec<_>>()
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert_eq!(
std::mem::size_of::<NumericFactorMap>(),
@@ -275,8 +323,8 @@ mod tests {
let flat: NumericFactorMap = input.clone().into_iter().collect();
let tree: BTreeMap<_, _> = input.into_iter().collect();
assert_eq!(
flat.iter().collect::<Vec<_>>(),
tree.iter().collect::<Vec<_>>()
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert_eq!(flat["z"], 4.0);
}
@@ -327,13 +375,14 @@ mod tests {
flat.extend(incoming.clone());
tree.extend(incoming);
assert_eq!(
flat.iter().collect::<Vec<_>>(),
tree.iter().collect::<Vec<_>>()
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
);
assert!(matches!(flat.keys().last(), Some(Cow::Borrowed("shared"))));
assert_eq!(flat.keys().last().map(CompactString::as_str), Some("shared"));
assert!(!flat.keys().last().unwrap().is_heap_allocated());
flat.extend([(Cow::Borrowed("zz"), f64::NAN)]);
assert!(flat["zz"].is_nan());
flat.extend(std::iter::empty());
flat.extend(std::iter::empty::<(CompactString, f64)>());
assert_eq!(flat.len(), tree.len() + 1);
}
}
@@ -4908,7 +4908,7 @@ impl PlatformExprStrategy {
.iter()
.filter(|(field, _)| {
self.stock_extra_factor_map_required
|| self.stock_extra_factor_identifiers.contains(field.as_ref())
|| self.stock_extra_factor_identifiers.contains(field.as_str())
})
.map(|(field, value)| (field.clone(), *value))
.collect()
@@ -5656,7 +5656,7 @@ impl PlatformExprStrategy {
Dynamic::from(stock.stock_volume_ma100),
);
for (key, value) in &stock.extra_factors {
factors.insert(key.as_ref().into(), Dynamic::from(*value));
factors.insert(key.as_str().into(), Dynamic::from(*value));
}
for (key, value) in &stock.extra_text_factors {
factors.insert(key.clone().into(), Dynamic::from(value.clone()));
@@ -17570,7 +17570,7 @@ mod tests {
assert_eq!(copied_state.extra_factors["negative_zero"].to_bits(), (-0.0_f64).to_bits());
assert!(copied_state.extra_factors["undefined_value"].is_nan());
let exposed = copied_state.extra_factors.iter()
.map(|(key, value)| (key.as_ref().into(), Dynamic::from(*value)))
.map(|(key, value)| (key.as_str().into(), Dynamic::from(*value)))
.collect::<Map>();
assert_eq!(exposed["negative_zero"].as_float().unwrap().to_bits(), (-0.0_f64).to_bits());
assert!(exposed["undefined_value"].as_float().unwrap().is_nan());