1use std::cmp::Ordering;
12
13use num_bigint::BigInt;
14use serde_json::Value;
15
16use crate::error::Result;
17use crate::library::Library;
18use crate::model::{FolderOrder, Item};
19
20const DIV_PLACES: u32 = 20;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub(crate) struct Dec {
26 m: BigInt,
27 s: u32,
28}
29
30fn pow10(n: u32) -> BigInt {
31 BigInt::from(10u8).pow(n)
32}
33
34impl Dec {
35 fn new(m: BigInt, s: u32) -> Dec {
36 let mut d = Dec { m, s };
37 let ten = BigInt::from(10u8);
38 let zero = BigInt::default();
39 while d.s > 0 && &d.m % &ten == zero {
40 d.m /= &ten;
41 d.s -= 1;
42 }
43 if d.m == zero {
44 d.s = 0;
45 }
46 d
47 }
48
49 pub(crate) fn from_int(v: i64) -> Dec {
50 Dec::new(BigInt::from(v), 0)
51 }
52
53 pub(crate) fn parse(text: &str) -> Option<Dec> {
55 let t = text.trim();
56 let (neg, t) = match t.strip_prefix('-') {
57 Some(rest) => (true, rest),
58 None => (false, t.strip_prefix('+').unwrap_or(t)),
59 };
60 let (num, exp) = match t.find(['e', 'E']) {
61 Some(i) => (&t[..i], t[i + 1..].parse::<i64>().ok()?),
62 None => (t, 0),
63 };
64 let (int, frac) = num.split_once('.').unwrap_or((num, ""));
65 if int.is_empty() && frac.is_empty() || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
66 return None;
67 }
68 let digits = format!("{int}{frac}");
69 let mut m: BigInt = digits.parse().ok()?;
70 if neg {
71 m = -m;
72 }
73 let scale = frac.len() as i64 - exp;
74 if scale >= 0 {
75 Some(Dec::new(m, u32::try_from(scale).ok()?))
76 } else {
77 Some(Dec::new(m * pow10(u32::try_from(-scale).ok()?), 0))
78 }
79 }
80
81 pub(crate) fn from_js_number(v: f64) -> Option<Dec> {
84 v.is_finite().then(|| Dec::parse(&format!("{v}"))).flatten()
85 }
86
87 fn aligned(&self, o: &Dec) -> (BigInt, BigInt, u32) {
88 let s = self.s.max(o.s);
89 (&self.m * pow10(s - self.s), &o.m * pow10(s - o.s), s)
90 }
91
92 pub(crate) fn plus(&self, o: &Dec) -> Dec {
93 let (a, b, s) = self.aligned(o);
94 Dec::new(a + b, s)
95 }
96
97 pub(crate) fn minus(&self, o: &Dec) -> Dec {
98 let (a, b, s) = self.aligned(o);
99 Dec::new(a - b, s)
100 }
101
102 pub(crate) fn times_int(&self, k: i64) -> Dec {
103 Dec::new(&self.m * BigInt::from(k), self.s)
104 }
105
106 pub(crate) fn div_int(&self, n: u64) -> Dec {
109 let num = &self.m * pow10(DIV_PLACES.saturating_sub(self.s));
110 let den = BigInt::from(n) * pow10(self.s.saturating_sub(DIV_PLACES));
111 let neg = num < BigInt::default();
112 let abs = if neg { -num } else { num };
113 let (q, r) = (&abs / &den, &abs % &den);
114 let q = if r * 2u8 >= den { q + 1u8 } else { q };
115 Dec::new(if neg { -q } else { q }, DIV_PLACES)
116 }
117
118 pub(crate) fn is_zero(&self) -> bool {
119 self.m == BigInt::default()
120 }
121
122 pub(crate) fn to_precision(&self) -> String {
125 if self.is_zero() {
126 return "0".into();
127 }
128 let neg = self.m < BigInt::default();
129 let digits = self.m.magnitude().to_string();
130 let e = digits.len() as i64 - 1 - i64::from(self.s);
131 let body = if e <= -7 || e >= 21 {
132 let (first, rest) = digits.split_at(1);
133 let rest = rest.trim_end_matches('0');
134 let dot = if rest.is_empty() { String::new() } else { format!(".{rest}") };
135 format!("{first}{dot}e{}{}", if e < 0 { "-" } else { "+" }, e.abs())
136 } else if self.s == 0 {
137 digits
138 } else if digits.len() > self.s as usize {
139 let (int, frac) = digits.split_at(digits.len() - self.s as usize);
140 format!("{int}.{frac}")
141 } else {
142 format!("0.{}{digits}", "0".repeat(self.s as usize - digits.len()))
143 };
144 if neg { format!("-{body}") } else { body }
145 }
146}
147
148fn entry_text(v: &Value) -> Option<String> {
152 match v {
153 Value::String(s) if !s.is_empty() => Some(s.clone()),
154 Value::Number(n) if n.as_f64() != Some(0.0) => Some(match n.as_f64() {
155 Some(f) if n.is_f64() => format!("{f}"),
156 _ => n.to_string(),
157 }),
158 _ => None,
159 }
160}
161
162impl Item {
163 pub fn manual_order_text(&self, folder: &str) -> Option<String> {
165 self.extra.get("order").and_then(|o| o.get(folder)).and_then(entry_text)
166 }
167
168 pub fn manual_order(&self, folder: &str) -> Option<f64> {
172 self.manual_order_text(folder).and_then(|t| t.trim().parse::<f64>().ok())
173 }
174
175 pub fn manual_sort_key(&self, folder: &str) -> String {
178 self.manual_order_text(folder).unwrap_or_else(|| self.modification_time.to_string())
179 }
180
181 pub fn set_manual_order(&mut self, folder: &str, value: &str) {
185 let slot = self.extra.entry("order").or_insert_with(|| Value::Object(Default::default()));
186 if !slot.is_object() {
187 *slot = Value::Object(Default::default());
188 }
189 if let Value::Object(m) = slot {
190 m.insert(folder.to_string(), Value::String(value.to_string()));
191 }
192 }
193
194 fn manual_dec(&self, folder: &str) -> Dec {
196 self.manual_order_text(folder).and_then(|t| Dec::parse(&t)).unwrap_or_else(|| Dec::from_int(self.modification_time as i64))
197 }
198}
199
200pub fn manual_cmp(a: &Item, b: &Item, folder: &str) -> Ordering {
203 b.manual_sort_key(folder).cmp(&a.manual_sort_key(folder))
204}
205
206impl Library {
207 pub fn move_items_manually(&mut self, folder: &str, moved: &[String], before: Option<&str>) -> Result<()> {
219 let Some(f) = self.all_folders().into_iter().find(|f| f.id == folder) else {
220 return Err(self.invalid(format!("no folder {folder}")));
221 };
222 let is_manual = f.order() == Some(FolderOrder::Manual);
223 let increase = if is_manual { f.sort_increase.unwrap_or(true) } else { true };
224 for id in moved {
225 match self.item(id) {
226 Some(i) if i.folders.iter().any(|x| x == folder) && !i.is_deleted => {}
227 _ => return Err(self.invalid(format!("item {id} is not in folder {folder}"))),
228 }
229 }
230 if before.is_some_and(|b| moved.iter().any(|m| m == b)) {
231 return Err(self.invalid("cannot move items before one of themselves"));
232 }
233
234 let mut view: Vec<&Item> = self.items().iter().filter(|i| !i.is_deleted && i.folders.iter().any(|x| x == folder)).collect();
235 crate::search::sort_manual(&mut view, folder, increase);
236 let (selected, rest): (Vec<&Item>, Vec<&Item>) = view.into_iter().partition(|i| moved.contains(&i.id));
237 let mut selected: Vec<Item> = selected.into_iter().cloned().collect();
238 let mut rest: Vec<Item> = rest.into_iter().cloned().collect();
239 let at = match before {
240 Some(b) => Some(rest.iter().position(|i| i.id == b).ok_or_else(|| self.invalid(format!("item {b} is not in folder {folder}")))?),
241 None => None,
242 };
243 if selected.is_empty() || rest.is_empty() {
244 return Ok(());
245 }
246
247 let mut changed: Vec<Item> = Vec::new();
249 let mut seen = std::collections::HashSet::new();
250 if rest.iter().filter_map(|i| i.manual_order_text(folder)).any(|t| !seen.insert(t)) {
251 let precision = rest.len().to_string().len() as i32 + 1;
252 let step = 1.0 / 10f64.powi(precision);
253 for (i, item) in rest.iter_mut().enumerate() {
254 let Some(old) = item.manual_order_text(folder).and_then(|t| Dec::parse(&t)) else { continue };
255 let Some(delta) = Dec::from_js_number(step * i as f64) else { continue };
258 item.set_manual_order(folder, &old.minus(&delta).to_precision());
259 changed.push(item.clone());
260 }
261 }
262
263 let one = Dec::from_int(1);
264 let n = selected.len() as u64;
265 let (middle, step) = match at {
266 Some(0) => {
267 let target = rest[0].manual_dec(folder);
268 let prev = if increase { target.plus(&one) } else { target.minus(&one) };
269 let middle = prev.plus(&target).div_int(2);
270 let step = middle.minus(&target).div_int(n);
271 (middle, step)
272 }
273 Some(k) => {
274 let (prev, target) = (rest[k - 1].manual_dec(folder), rest[k].manual_dec(folder));
275 let middle = prev.plus(&target).div_int(2);
276 let step = middle.minus(&target).div_int(n);
277 (middle, step)
278 }
279 None => {
280 let prev = rest[rest.len() - 1].manual_dec(folder);
281 let target = if increase { prev.minus(&one) } else { prev.plus(&one) };
282 let middle = target.plus(&prev).div_int(2);
283 let step = middle.minus(&target).div_int(n);
284 (middle, step)
285 }
286 };
287 if !step.is_zero() {
288 for (index, item) in selected.iter_mut().rev().enumerate() {
289 item.set_manual_order(folder, &middle.plus(&step.times_int(index as i64)).to_precision());
290 }
291 changed.extend(selected);
292 }
293 for item in changed {
294 self.update_item(item)?;
295 }
296 if !is_manual {
297 self.set_folder_order(folder, Some(FolderOrder::Manual), true)?;
298 }
299 Ok(())
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn d(s: &str) -> Dec {
308 Dec::parse(s).unwrap()
309 }
310
311 #[test]
312 fn decimals_print_like_to_precision() {
313 assert_eq!(d("1789479235636").to_precision(), "1789479235636");
314 assert_eq!(d("1789479235636.50").to_precision(), "1789479235636.5");
315 assert_eq!(d("-0.25").to_precision(), "-0.25");
316 assert_eq!(d("0.0000001").to_precision(), "1e-7");
317 assert_eq!(d("0.0000012").to_precision(), "0.0000012");
318 assert_eq!(d("1.5e-8").to_precision(), "1.5e-8");
319 assert_eq!(d("123e19").to_precision(), "1.23e+21");
320 assert_eq!(d("0").to_precision(), "0");
321 assert_eq!(Dec::parse("abc"), None);
322 assert_eq!(Dec::parse(""), None);
323 }
324
325 #[test]
326 fn division_keeps_twenty_places_rounding_half_away() {
327 assert_eq!(d("1").div_int(3).to_precision(), "0.33333333333333333333");
328 assert_eq!(d("2").div_int(3).to_precision(), "0.66666666666666666667");
329 assert_eq!(d("-2").div_int(3).to_precision(), "-0.66666666666666666667");
330 assert_eq!(d("3579000000001").div_int(2).to_precision(), "1789500000000.5");
331 }
332
333 #[test]
334 fn js_numbers_bring_their_float_noise() {
335 assert_eq!(Dec::from_js_number(0.01 * 57.0).unwrap().to_precision(), "0.5700000000000001");
336 assert_eq!(Dec::from_js_number(0.01 * 3.0).unwrap().to_precision(), "0.03");
337 }
338
339 fn record(id: &str, mtime: u64, extra: &str) -> Item {
340 Item::from_json_str(&format!(
341 r#"{{"id":"{id}","name":"{id}","size":1,"btime":1,"mtime":1,"ext":"tex","tags":[],"folders":["F"],"isDeleted":false,"url":"","annotation":"","modificationTime":{mtime},"lastModified":1{extra}}}"#
342 ))
343 .unwrap()
344 }
345
346 fn library(dir: &std::path::Path, items: Vec<Item>) -> (Library, String) {
348 let mut lib = Library::create(dir.join("T.library")).unwrap();
349 let f = lib.create_folder(None, "F").unwrap();
350 for mut item in items {
351 item.folders = vec![f.clone()];
352 std::fs::create_dir_all(item.info_dir(lib.root())).unwrap();
353 lib.register_item(item).unwrap();
354 }
355 (lib, f)
356 }
357
358 fn view(lib: &Library, f: &str) -> Vec<String> {
359 let mut v: Vec<&Item> = lib.items().iter().filter(|i| i.folders.iter().any(|x| x == f)).collect();
360 crate::search::sort_manual(&mut v, f, true);
361 v.iter().map(|i| i.id.clone()).collect()
362 }
363
364 fn ids(list: &[&str]) -> Vec<String> {
365 list.iter().map(|s| s.to_string()).collect()
366 }
367
368 #[test]
369 fn manual_sort_compares_text_largest_first() {
370 let tmp = tempfile::tempdir().unwrap();
371 let mut b = record("B", 1000, "");
372 b.set_manual_order("X", "999");
373 let (lib, f) = library(tmp.path(), vec![record("A", 2000, ""), b]);
374 assert_eq!(view(&lib, &f), ids(&["A", "B"]));
376 let b = lib.item("B").unwrap();
377 assert_eq!((b.manual_order("X"), b.manual_order(&f)), (Some(999.0), None));
378 let mut x: Vec<&Item> = lib.items().iter().collect();
380 crate::search::sort_manual(&mut x, "X", true);
381 assert_eq!(x[0].id, "B");
382 }
383
384 #[test]
385 fn dropping_between_neighbours_takes_the_midpoint() {
386 let tmp = tempfile::tempdir().unwrap();
387 let (mut lib, f) = library(tmp.path(), vec![record("A", 1000, ""), record("B", 2000, ""), record("C", 3000, "")]);
388 assert_eq!(view(&lib, &f), ids(&["C", "B", "A"]));
389 lib.move_items_manually(&f, &ids(&["A"]), Some("B")).unwrap();
390 assert_eq!(lib.item("A").unwrap().manual_order_text(&f).as_deref(), Some("2500"));
391 assert_eq!(view(&lib, &f), ids(&["C", "A", "B"]));
392 let folder = lib.all_folders().into_iter().find(|x| x.id == f).unwrap();
393 assert_eq!((folder.order(), folder.sort_increase), (Some(FolderOrder::Manual), Some(true)));
394
395 lib.move_items_manually(&f, &ids(&["B", "A"]), Some("C")).unwrap();
397 assert_eq!(view(&lib, &f), ids(&["A", "B", "C"]));
398 assert_eq!(lib.item("B").unwrap().manual_order_text(&f).as_deref(), Some("3000.5"));
399 assert_eq!(lib.item("A").unwrap().manual_order_text(&f).as_deref(), Some("3000.75"));
400
401 lib.move_items_manually(&f, &ids(&["A"]), None).unwrap();
403 assert_eq!(view(&lib, &f), ids(&["B", "C", "A"]));
404 assert_eq!(lib.item("A").unwrap().manual_order_text(&f).as_deref(), Some("2999.5"));
405
406 let reopened = Library::open(lib.root()).unwrap();
408 assert_eq!(view(&reopened, &f), ids(&["B", "C", "A"]));
409 let text = std::fs::read_to_string(reopened.item("A").unwrap().info_dir(reopened.root()).join("metadata.json")).unwrap();
410 assert!(text.ends_with(&format!(r#","order":{{"{f}":"2999.5"}}}}"#)), "{text}");
411 assert!(!text.contains("width"), "0×0 records stay without dimensions: {text}");
412
413 assert!(lib.move_items_manually(&f, &ids(&["A"]), Some("A")).is_err());
414 assert!(lib.move_items_manually(&f, &ids(&["nope"]), None).is_err());
415 }
416
417 #[test]
418 fn three_way_split_rounds_to_twenty_places() {
419 let tmp = tempfile::tempdir().unwrap();
420 let (mut lib, f) = library(tmp.path(), vec![record("A", 1000, ""), record("B", 1001, ""), record("C", 3000, ""), record("D", 3001, ""), record("E", 3002, "")]);
421 lib.move_items_manually(&f, &ids(&["C", "D", "E"]), Some("A")).unwrap();
423 assert_eq!(view(&lib, &f), ids(&["B", "E", "D", "C", "A"]));
424 let text = |id: &str| lib.item(id).unwrap().manual_order_text(&f).unwrap();
425 assert_eq!(text("C"), "1000.5");
426 assert_eq!(text("D"), "1000.66666666666666666667");
427 assert_eq!(text("E"), "1000.83333333333333333334");
428 }
429
430 #[test]
431 fn duplicate_values_are_nudged_apart_first() {
432 let tmp = tempfile::tempdir().unwrap();
433 let mut a = record("A", 1, "");
434 let mut b = record("B", 2, "");
435 a.set_manual_order("F0", "5000");
436 b.set_manual_order("F0", "5000");
437 let (mut lib, f) = library(tmp.path(), vec![a, b, record("C", 9000, ""), record("D", 1000, "")]);
438 for id in ["A", "B"] {
439 let mut item = lib.item(id).unwrap().clone();
440 item.extra.remove("order");
441 item.set_manual_order(&f, "5000");
442 lib.update_item(item).unwrap();
443 }
444 assert_eq!(view(&lib, &f), ids(&["C", "A", "B", "D"]));
446 lib.move_items_manually(&f, &ids(&["D"]), Some("C")).unwrap();
447 let text = |id: &str| lib.item(id).unwrap().manual_order_text(&f).unwrap();
449 assert_eq!(text("A"), "4999.99");
450 assert_eq!(text("B"), "4999.98");
451 assert_eq!(text("D"), "9000.5");
452 assert_eq!(view(&lib, &f), ids(&["D", "C", "A", "B"]));
453 }
454
455 #[test]
456 fn descending_folders_place_against_the_other_side() {
457 let tmp = tempfile::tempdir().unwrap();
458 let (mut lib, f) = library(tmp.path(), vec![record("A", 1000, ""), record("B", 2000, "")]);
459 lib.set_folder_order(&f, Some(FolderOrder::Manual), false).unwrap();
460 lib.move_items_manually(&f, &ids(&["B"]), Some("A")).unwrap();
462 assert_eq!(lib.item("B").unwrap().manual_order_text(&f).as_deref(), Some("999.5"));
463 let folder = lib.all_folders().into_iter().find(|x| x.id == f).unwrap();
464 assert_eq!(folder.sort_increase, Some(false), "a manual folder keeps its direction");
465 }
466
467 #[test]
468 fn existing_order_maps_keep_their_place() {
469 let json = r#"{"id":"A","name":"a","size":1,"btime":1,"mtime":1,"ext":"md","tags":[],"folders":["F1"],"isDeleted":false,"url":"","annotation":"","modificationTime":3,"order":{"F1":"1789479235636.5"},"lastModified":4}"#;
470 let mut item = Item::from_json_str(json).unwrap();
471 assert_eq!(serde_json::to_string(&item).unwrap(), json);
472 assert_eq!(item.manual_order("F1"), Some(1789479235636.5));
473 item.set_manual_order("F1", "7");
474 item.set_manual_order("F2", "8");
475 let out = serde_json::to_string(&item).unwrap();
476 assert!(out.contains(r#""modificationTime":3,"order":{"F1":"7","F2":"8"},"lastModified":4}"#), "{out}");
477 }
478}