use crate::config::{SortKey, SortRule};
use serde::{Deserialize, Serialize};
use std::sync::Once;
static SORT_VALUE_WARNING: Once = Once::new();
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct LegacySortValue {
pub priority_none: Option<u8>,
pub priority_low: Option<u8>,
pub priority_medium: Option<u8>,
pub priority_high: Option<u8>,
pub no_due_date: Option<u8>,
pub not_recurring: Option<u8>,
pub today: Option<u8>,
pub overdue: Option<u8>,
pub now: Option<u8>,
pub deadline_value: Option<u8>,
pub deadline_days: Option<u8>,
}
pub(crate) fn detect_and_migrate_sort_value(
sort_value: Option<&LegacySortValue>,
) -> Option<Vec<SortRule>> {
let sort_value = sort_value?;
SORT_VALUE_WARNING.call_once(|| {
eprintln!(
"Legacy sort_value config detected; migrating to sort_order. \
Please update your config because sort_value will be removed in a future version."
);
});
let priority = [
sort_value.priority_none,
sort_value.priority_low,
sort_value.priority_medium,
sort_value.priority_high,
]
.into_iter()
.flatten()
.max()
.unwrap_or_default();
let mut weighted_keys = vec![
(SortKey::Priority, priority),
(SortKey::Overdue, sort_value.overdue.unwrap_or_default()),
(SortKey::Today, sort_value.today.unwrap_or_default()),
(SortKey::Now, sort_value.now.unwrap_or_default()),
(
SortKey::NoDueDate,
sort_value.no_due_date.unwrap_or_default(),
),
(
SortKey::NotRecurring,
sort_value.not_recurring.unwrap_or_default(),
),
(
SortKey::Deadline,
sort_value.deadline_value.unwrap_or_default(),
),
];
weighted_keys.sort_by(|(left_key, left_weight), (right_key, right_weight)| {
right_weight.cmp(left_weight).then_with(|| {
sort_key_default_index(*left_key).cmp(&sort_key_default_index(*right_key))
})
});
let mut keys: Vec<SortKey> = weighted_keys.into_iter().map(|(key, _)| key).collect();
for default_key in SortKey::default_order() {
if !keys.contains(&default_key) {
keys.push(default_key);
}
}
Some(
keys.into_iter()
.map(SortRule::with_default_direction)
.collect(),
)
}
fn sort_key_default_index(key: SortKey) -> usize {
SortKey::default_order()
.iter()
.position(|default_key| *default_key == key)
.unwrap_or(usize::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_and_migrate_sort_value_returns_none_for_missing_input() {
assert_eq!(detect_and_migrate_sort_value(None), None);
}
#[test]
fn sort_key_default_index_returns_zero_for_first_key() {
assert_eq!(sort_key_default_index(SortKey::Priority), 0);
}
#[test]
fn sort_key_default_index_returns_last_for_last_key() {
let keys = SortKey::default_order();
let last = keys.last().expect("default order should not be empty");
assert_eq!(sort_key_default_index(*last), keys.len() - 1);
}
#[test]
fn sort_key_default_index_falls_back_to_usize_max_for_unknown_key() {
for key in &[
SortKey::Priority,
SortKey::Overdue,
SortKey::Today,
SortKey::Now,
SortKey::NoDueDate,
SortKey::NotRecurring,
SortKey::Deadline,
] {
assert!(
sort_key_default_index(*key) < usize::MAX,
"{key:?} should be in default_order"
);
}
}
#[test]
fn detect_and_migrate_sort_value_prioritizes_highest_weighted_key() {
let legacy = LegacySortValue {
priority_none: Some(0),
priority_low: Some(0),
priority_medium: Some(0),
priority_high: Some(0),
no_due_date: Some(0),
not_recurring: Some(0),
today: Some(0),
overdue: Some(9),
now: Some(0),
deadline_value: Some(1),
deadline_days: Some(0),
};
let migrated = detect_and_migrate_sort_value(Some(&legacy))
.expect("legacy sort config should migrate");
let first = migrated
.first()
.expect("migrated sort order should not be empty");
assert_eq!(first.key, SortKey::Overdue);
}
}