1use std::cmp::Ordering;
24
25use hidane_proto::google::{
26 firestore::v1::{ArrayValue, MapValue, Value, value::ValueType},
27 r#type::LatLng,
28};
29use prost_types::Timestamp;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub enum TypeRank {
35 Null,
36 Boolean,
37 Number,
38 Timestamp,
39 String,
40 Bytes,
41 Reference,
42 GeoPoint,
43 Array,
44 Vector,
45 Map,
46 Expression,
49}
50
51pub(crate) const VECTOR_TYPE_KEY: &str = "__type__";
52pub(crate) const VECTOR_TYPE: &str = "__vector__";
53pub(crate) const VECTOR_VALUES_KEY: &str = "value";
54
55pub fn is_vector(map: &MapValue) -> bool {
57 matches!(
58 map.fields.get(VECTOR_TYPE_KEY).and_then(|v| v.value_type.as_ref()),
59 Some(ValueType::StringValue(t)) if t == VECTOR_TYPE
60 )
61}
62
63pub fn vector_elements(map: &MapValue) -> &[Value] {
65 match map
66 .fields
67 .get(VECTOR_VALUES_KEY)
68 .and_then(|v| v.value_type.as_ref())
69 {
70 Some(ValueType::ArrayValue(array)) => &array.values,
71 _ => &[],
72 }
73}
74
75pub fn type_rank(value: &Value) -> TypeRank {
76 match &value.value_type {
77 None | Some(ValueType::NullValue(_)) => TypeRank::Null,
78 Some(ValueType::BooleanValue(_)) => TypeRank::Boolean,
79 Some(ValueType::IntegerValue(_) | ValueType::DoubleValue(_)) => TypeRank::Number,
80 Some(ValueType::TimestampValue(_)) => TypeRank::Timestamp,
81 Some(ValueType::StringValue(_)) => TypeRank::String,
82 Some(ValueType::BytesValue(_)) => TypeRank::Bytes,
83 Some(ValueType::ReferenceValue(_)) => TypeRank::Reference,
84 Some(ValueType::GeoPointValue(_)) => TypeRank::GeoPoint,
85 Some(ValueType::ArrayValue(_)) => TypeRank::Array,
86 Some(ValueType::MapValue(map)) if is_vector(map) => TypeRank::Vector,
87 Some(ValueType::MapValue(_)) => TypeRank::Map,
88 Some(
89 ValueType::FieldReferenceValue(_)
90 | ValueType::VariableReferenceValue(_)
91 | ValueType::FunctionValue(_)
92 | ValueType::PipelineValue(_),
93 ) => TypeRank::Expression,
94 }
95}
96
97pub fn compare(a: &Value, b: &Value) -> Ordering {
99 let (rank_a, rank_b) = (type_rank(a), type_rank(b));
100 if rank_a != rank_b {
101 return rank_a.cmp(&rank_b);
102 }
103 match (&a.value_type, &b.value_type) {
104 (Some(ValueType::BooleanValue(x)), Some(ValueType::BooleanValue(y))) => x.cmp(y),
105 (Some(x), Some(y)) if rank_a == TypeRank::Number => compare_numbers(x, y),
106 (Some(ValueType::TimestampValue(x)), Some(ValueType::TimestampValue(y))) => {
107 compare_timestamps(x, y)
108 }
109 (Some(ValueType::StringValue(x)), Some(ValueType::StringValue(y))) => x.cmp(y),
110 (Some(ValueType::BytesValue(x)), Some(ValueType::BytesValue(y))) => x.cmp(y),
111 (Some(ValueType::ReferenceValue(x)), Some(ValueType::ReferenceValue(y))) => {
112 compare_paths(x.split('/'), y.split('/'))
113 }
114 (Some(ValueType::GeoPointValue(x)), Some(ValueType::GeoPointValue(y))) => {
115 compare_geo_points(x, y)
116 }
117 (Some(ValueType::ArrayValue(x)), Some(ValueType::ArrayValue(y))) => compare_arrays(x, y),
118 (Some(ValueType::MapValue(x)), Some(ValueType::MapValue(y)))
119 if rank_a == TypeRank::Vector =>
120 {
121 compare_vectors(x, y)
122 }
123 (Some(ValueType::MapValue(x)), Some(ValueType::MapValue(y))) => compare_maps(x, y),
124 _ => Ordering::Equal,
126 }
127}
128
129#[derive(Debug, Clone, Copy)]
131pub enum Number {
132 Integer(i64),
133 Double(f64),
134}
135
136impl Number {
137 pub fn of(value: &ValueType) -> Option<Self> {
138 match value {
139 ValueType::IntegerValue(i) => Some(Self::Integer(*i)),
140 ValueType::DoubleValue(d) => Some(Self::Double(*d)),
141 _ => None,
142 }
143 }
144}
145
146fn compare_numbers(a: &ValueType, b: &ValueType) -> Ordering {
147 match (Number::of(a), Number::of(b)) {
148 (Some(a), Some(b)) => compare_number(a, b),
149 _ => Ordering::Equal,
150 }
151}
152
153pub fn compare_number(a: Number, b: Number) -> Ordering {
154 match (a, b) {
155 (Number::Integer(x), Number::Integer(y)) => x.cmp(&y),
156 (Number::Double(x), Number::Double(y)) => compare_doubles(x, y),
157 (Number::Integer(x), Number::Double(y)) => compare_integer_double(x, y),
158 (Number::Double(x), Number::Integer(y)) => compare_integer_double(y, x).reverse(),
159 }
160}
161
162pub fn compare_doubles(a: f64, b: f64) -> Ordering {
164 match (a.is_nan(), b.is_nan()) {
165 (true, true) => Ordering::Equal,
166 (true, false) => Ordering::Less,
167 (false, true) => Ordering::Greater,
168 (false, false) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
169 }
170}
171
172pub fn compare_integer_double(i: i64, d: f64) -> Ordering {
174 const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
175 if d.is_nan() {
176 return Ordering::Greater;
177 }
178 if d >= TWO_POW_63 {
179 return Ordering::Less;
180 }
181 if d < -TWO_POW_63 {
182 return Ordering::Greater;
183 }
184 let whole = d.trunc();
186 #[allow(clippy::cast_possible_truncation)]
187 let whole_int = whole as i64;
188 i.cmp(&whole_int).then_with(|| {
189 let fraction = d - whole;
190 if fraction > 0.0 {
191 Ordering::Less
192 } else if fraction < 0.0 {
193 Ordering::Greater
194 } else {
195 Ordering::Equal
196 }
197 })
198}
199
200pub fn compare_timestamps(a: &Timestamp, b: &Timestamp) -> Ordering {
201 (a.seconds, a.nanos).cmp(&(b.seconds, b.nanos))
202}
203
204fn compare_geo_points(a: &LatLng, b: &LatLng) -> Ordering {
205 compare_doubles(a.latitude, b.latitude).then_with(|| compare_doubles(a.longitude, b.longitude))
206}
207
208fn compare_arrays(a: &ArrayValue, b: &ArrayValue) -> Ordering {
209 compare_sequences(&a.values, &b.values)
210}
211
212fn compare_sequences(a: &[Value], b: &[Value]) -> Ordering {
213 a.iter()
214 .zip(b)
215 .map(|(x, y)| compare(x, y))
216 .find(|o| o.is_ne())
217 .unwrap_or_else(|| a.len().cmp(&b.len()))
218}
219
220fn compare_vectors(a: &MapValue, b: &MapValue) -> Ordering {
221 let (x, y) = (vector_elements(a), vector_elements(b));
222 x.len().cmp(&y.len()).then_with(|| compare_sequences(x, y))
223}
224
225fn compare_maps(a: &MapValue, b: &MapValue) -> Ordering {
226 a.fields
228 .sorted()
229 .into_iter()
230 .zip(b.fields.sorted())
231 .map(|((ka, va), (kb, vb))| ka.cmp(kb).then_with(|| compare(va, vb)))
232 .find(|o| o.is_ne())
233 .unwrap_or_else(|| a.fields.len().cmp(&b.fields.len()))
234}
235
236pub fn numeric_id(segment: &str) -> Option<i64> {
240 let digits = segment.strip_prefix("__id")?.strip_suffix("__")?;
241 let magnitude = digits.strip_prefix('-').unwrap_or(digits);
242 let canonical = match magnitude.as_bytes() {
243 [b'0'] => magnitude.len() == digits.len(),
244 [] | [b'0', ..] => false,
245 bytes => bytes.iter().all(u8::is_ascii_digit),
246 };
247 if canonical { digits.parse().ok() } else { None }
248}
249
250pub fn compare_segments(a: &str, b: &str) -> Ordering {
252 match (numeric_id(a), numeric_id(b)) {
253 (Some(x), Some(y)) => x.cmp(&y),
254 (Some(_), None) => Ordering::Less,
255 (None, Some(_)) => Ordering::Greater,
256 (None, None) => a.cmp(b),
257 }
258}
259
260pub fn compare_paths<'a>(
262 a: impl IntoIterator<Item = &'a str>,
263 b: impl IntoIterator<Item = &'a str>,
264) -> Ordering {
265 let (mut a, mut b) = (a.into_iter(), b.into_iter());
266 loop {
267 match (a.next(), b.next()) {
268 (Some(x), Some(y)) => match compare_segments(x, y) {
269 Ordering::Equal => {}
270 other => return other,
271 },
272 (None, None) => return Ordering::Equal,
273 (None, Some(_)) => return Ordering::Less,
274 (Some(_), None) => return Ordering::Greater,
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::numeric_id;
282
283 #[test]
284 fn numeric_ids_are_written_one_way() {
285 for (segment, expected) in [
286 ("__id5__", Some(5)),
287 ("__id-3__", Some(-3)),
288 ("__id0__", Some(0)),
289 ("__id9223372036854775807__", Some(i64::MAX)),
290 ("__id-9223372036854775808__", Some(i64::MIN)),
291 ("__id-0__", None),
292 ("__id007__", None),
293 ("__id+5__", None),
294 ("__id__", None),
295 ("__id-__", None),
296 ("__id 5__", None),
297 ("__id9223372036854775808__", None),
298 ("id5", None),
299 ] {
300 assert_eq!(numeric_id(segment), expected, "{segment}");
301 }
302 }
303}