1use std::cmp::Ordering;
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, PartialEq)]
15pub struct BaseLink {
16 pub path: String,
19 pub display: Option<String>,
21}
22
23impl BaseLink {
24 pub fn new(path: impl Into<String>) -> Self {
25 Self {
26 path: path.into(),
27 display: None,
28 }
29 }
30
31 pub fn with_display(path: impl Into<String>, display: impl Into<String>) -> Self {
32 Self {
33 path: path.into(),
34 display: Some(display.into()),
35 }
36 }
37
38 pub fn basename(&self) -> &str {
42 let no_dir = self.path.rsplit('/').next().unwrap_or(&self.path);
43 no_dir.strip_suffix(".md").unwrap_or(no_dir)
44 }
45
46 pub fn same_target(&self, other: &BaseLink) -> bool {
49 if self.path == other.path {
50 return true;
51 }
52 self.basename().eq_ignore_ascii_case(other.basename())
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct BaseDate {
61 pub year: i64,
62 pub month: u32, pub day: u32, pub hour: u32,
65 pub minute: u32,
66 pub second: u32,
67 pub millisecond: u32,
68 pub has_time: bool,
71}
72
73impl BaseDate {
74 fn days_from_epoch(&self) -> i64 {
78 let y = if self.month <= 2 {
79 self.year - 1
80 } else {
81 self.year
82 };
83 let era = if y >= 0 { y } else { y - 399 } / 400;
84 let yoe = y - era * 400; let m = self.month as i64;
86 let d = self.day as i64;
87 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
90 }
91
92 pub fn epoch_millis(&self) -> i64 {
95 let days = self.days_from_epoch();
96 let secs =
97 days * 86400 + self.hour as i64 * 3600 + self.minute as i64 * 60 + self.second as i64;
98 secs * 1000 + self.millisecond as i64
99 }
100}
101
102impl PartialOrd for BaseDate {
103 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
104 Some(self.epoch_millis().cmp(&other.epoch_millis()))
105 }
106}
107
108#[derive(Debug, Clone)]
110pub enum Value {
111 Null,
112 Bool(bool),
113 Number(f64),
114 Str(String),
115 Date(BaseDate),
116 Duration(i64),
118 List(Vec<Value>),
119 Object(BTreeMap<String, Value>),
120 Link(BaseLink),
121}
122
123impl Value {
124 pub fn type_name(&self) -> &'static str {
126 match self {
127 Value::Null => "null",
128 Value::Bool(_) => "boolean",
129 Value::Number(_) => "number",
130 Value::Str(_) => "string",
131 Value::Date(_) => "date",
132 Value::Duration(_) => "duration",
133 Value::List(_) => "list",
134 Value::Object(_) => "object",
135 Value::Link(_) => "link",
136 }
137 }
138
139 pub fn is_truthy(&self) -> bool {
142 match self {
143 Value::Null => false,
144 Value::Bool(b) => *b,
145 Value::Number(n) => *n != 0.0 && !n.is_nan(),
146 Value::Str(s) => !s.is_empty(),
147 Value::Duration(d) => *d != 0,
148 Value::List(l) => !l.is_empty(),
149 Value::Object(o) => !o.is_empty(),
150 Value::Date(_) | Value::Link(_) => true,
151 }
152 }
153
154 pub fn is_empty(&self) -> bool {
157 match self {
158 Value::Null => true,
159 Value::Str(s) => s.is_empty(),
160 Value::List(l) => l.is_empty(),
161 Value::Object(o) => o.is_empty(),
162 _ => false,
163 }
164 }
165
166 pub fn as_number(&self) -> Option<f64> {
170 match self {
171 Value::Number(n) => Some(*n),
172 Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
173 Value::Str(s) => s.trim().parse::<f64>().ok(),
174 Value::Duration(d) => Some(*d as f64),
175 _ => None,
176 }
177 }
178
179 pub fn as_str_coerced(&self) -> String {
181 self.display()
182 }
183
184 pub fn display(&self) -> String {
188 match self {
189 Value::Null => String::new(),
190 Value::Bool(b) => b.to_string(),
191 Value::Number(n) => format_number(*n),
192 Value::Str(s) => s.clone(),
193 Value::Date(d) => crate::format::default_date(d),
194 Value::Duration(ms) => format!("{ms}ms"),
195 Value::List(items) => items
196 .iter()
197 .map(Value::display)
198 .collect::<Vec<_>>()
199 .join(", "),
200 Value::Object(map) => map
201 .iter()
202 .map(|(k, v)| format!("{k}: {}", v.display()))
203 .collect::<Vec<_>>()
204 .join(", "),
205 Value::Link(l) => l
206 .display
207 .clone()
208 .unwrap_or_else(|| l.basename().to_string()),
209 }
210 }
211}
212
213pub fn format_number(n: f64) -> String {
216 if n.is_nan() {
217 return "NaN".to_string();
218 }
219 if n.is_infinite() {
220 return if n > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
221 }
222 if n == n.trunc() && n.abs() < 1e15 {
223 format!("{}", n as i64)
224 } else {
225 let s = format!("{n}");
227 s
228 }
229}
230
231impl Value {
232 pub fn loose_eq(&self, other: &Value) -> bool {
236 match (self, other) {
237 (Value::Null, Value::Null) => true,
238 (Value::Bool(a), Value::Bool(b)) => a == b,
239 (Value::Number(a), Value::Number(b)) => a == b,
240 (Value::Str(a), Value::Str(b)) => a == b,
241 (Value::Date(a), Value::Date(b)) => a.epoch_millis() == b.epoch_millis(),
242 (Value::Duration(a), Value::Duration(b)) => a == b,
243 (Value::Link(a), Value::Link(b)) => a.same_target(b),
244 (Value::Link(a), Value::Str(b)) | (Value::Str(b), Value::Link(a)) => {
247 a.path == *b || a.basename().eq_ignore_ascii_case(b)
248 }
249 (Value::List(a), Value::List(b)) => {
250 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.loose_eq(y))
251 }
252 _ => match (self.as_number(), other.as_number()) {
254 (Some(a), Some(b))
255 if matches!(self, Value::Number(_) | Value::Bool(_))
256 || matches!(other, Value::Number(_) | Value::Bool(_)) =>
257 {
258 a == b
259 }
260 _ => false,
261 },
262 }
263 }
264
265 pub fn loose_cmp(&self, other: &Value) -> Ordering {
270 match (self, other) {
271 (Value::Null, Value::Null) => Ordering::Equal,
272 (Value::Null, _) => Ordering::Greater,
273 (_, Value::Null) => Ordering::Less,
274 (Value::Number(a), Value::Number(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
275 (Value::Date(a), Value::Date(b)) => a.epoch_millis().cmp(&b.epoch_millis()),
276 (Value::Duration(a), Value::Duration(b)) => a.cmp(b),
277 (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
278 (Value::Str(a), Value::Str(b)) => a.to_lowercase().cmp(&b.to_lowercase()),
279 (Value::Link(a), Value::Link(b)) => {
280 a.display().to_lowercase().cmp(&b.display().to_lowercase())
281 }
282 _ => {
283 match (self.as_number(), other.as_number()) {
285 (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
286 _ => self
287 .display()
288 .to_lowercase()
289 .cmp(&other.display().to_lowercase()),
290 }
291 }
292 }
293 }
294}
295
296impl BaseLink {
297 fn display(&self) -> String {
298 self.display
299 .clone()
300 .unwrap_or_else(|| self.basename().to_string())
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn truthiness_matches_obsidian() {
310 assert!(!Value::Null.is_truthy());
311 assert!(!Value::Bool(false).is_truthy());
312 assert!(!Value::Number(0.0).is_truthy());
313 assert!(!Value::Str(String::new()).is_truthy());
314 assert!(!Value::List(vec![]).is_truthy());
315 assert!(Value::Number(1.0).is_truthy());
316 assert!(Value::Str("x".into()).is_truthy());
317 assert!(Value::List(vec![Value::Null]).is_truthy());
318 }
319
320 #[test]
321 fn number_formatting_drops_integer_decimal() {
322 assert_eq!(format_number(3.0), "3");
323 assert_eq!(format_number(3.25), "3.25");
324 assert_eq!(format_number(-5.0), "-5");
325 }
326
327 #[test]
328 fn link_basename_and_same_target() {
329 let a = BaseLink::new("Categories/Books");
330 let b = BaseLink::new("Books");
331 assert_eq!(a.basename(), "Books");
332 assert!(a.same_target(&b));
333 let c = BaseLink::with_display("Categories/Books", "Books");
334 assert_eq!(c.display, Some("Books".to_string()));
335 }
336
337 #[test]
338 fn link_string_loose_equality_by_basename() {
339 let link = Value::Link(BaseLink::new("Categories/Books"));
340 assert!(link.loose_eq(&Value::Str("Books".into())));
341 assert!(link.loose_eq(&Value::Str("Categories/Books".into())));
342 assert!(!link.loose_eq(&Value::Str("Films".into())));
343 }
344
345 #[test]
346 fn numeric_string_cross_equality() {
347 assert!(Value::Number(3.0).loose_eq(&Value::Str("3".into())));
348 assert!(!Value::Str("3".into()).loose_eq(&Value::Str("3.0".into())));
349 }
350
351 #[test]
352 fn epoch_millis_reference_dates() {
353 let epoch = BaseDate {
354 year: 1970,
355 month: 1,
356 day: 1,
357 hour: 0,
358 minute: 0,
359 second: 0,
360 millisecond: 0,
361 has_time: false,
362 };
363 assert_eq!(epoch.epoch_millis(), 0);
364 let d = BaseDate {
365 year: 2000,
366 month: 1,
367 day: 1,
368 hour: 0,
369 minute: 0,
370 second: 0,
371 millisecond: 0,
372 has_time: false,
373 };
374 assert_eq!(d.epoch_millis(), 946_684_800_000);
376 }
377
378 #[test]
379 fn date_ordering() {
380 let a = BaseDate {
381 year: 2020,
382 month: 5,
383 day: 1,
384 hour: 0,
385 minute: 0,
386 second: 0,
387 millisecond: 0,
388 has_time: false,
389 };
390 let b = BaseDate {
391 year: 2021,
392 month: 1,
393 day: 1,
394 hour: 0,
395 minute: 0,
396 second: 0,
397 millisecond: 0,
398 has_time: false,
399 };
400 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
401 }
402
403 #[test]
404 fn null_sorts_last() {
405 assert_eq!(
406 Value::Null.loose_cmp(&Value::Number(1.0)),
407 Ordering::Greater
408 );
409 assert_eq!(Value::Number(1.0).loose_cmp(&Value::Null), Ordering::Less);
410 }
411}