dioxus_sortable/
use_sorter.rs

1use dioxus::prelude::*;
2use std::cmp::Ordering;
3
4/// Stores Dioxus hooks and state of our sortable items.
5#[derive(Copy, Clone, Debug, PartialEq)]
6pub struct UseSorter<'a, F: 'static> {
7    field: &'a UseState<F>,
8    direction: &'a UseState<Direction>,
9}
10
11/// Trait used by [UseSorter](UseSorter) to sort a struct by a specific field. This must be implemented on the field enum. Type `T` represents the struct (table row) that is being sorted.
12///
13/// The implementation should use the [`PartialOrd::partial_cmp`] trait to compare the field values and return the result. For example:
14/// ```rust
15/// # use dioxus_sortable::PartialOrdBy;
16/// # #[derive(PartialEq)]
17/// struct MyStruct {
18///     first: String,
19///     second: f64, // <- Note: can return None if f64::NAN
20/// }
21///
22/// # #[derive(Copy, Clone, Debug, PartialEq)]
23/// enum MyStructField {
24///     First,
25///     Second,
26/// }
27///
28/// impl PartialOrdBy<MyStruct> for MyStructField {
29///     fn partial_cmp_by(&self, a: &MyStruct, b: &MyStruct) -> Option<std::cmp::Ordering> {
30///         match self {
31///             MyStructField::First => a.first.partial_cmp(&b.first),
32///             MyStructField::Second => a.second.partial_cmp(&b.second),
33///         }
34///     }
35/// }
36/// ```
37///
38/// Be careful when using [`Option::None`] or a custom enum to represent missing data (`NULL` values). As `partial_cmp` as `None` is less than `Some`:
39///
40/// ```rust
41/// # use std::cmp::Ordering;
42/// assert_eq!(Ordering::Less, None.cmp(&Some(0)));
43/// ```
44///
45pub trait PartialOrdBy<T>: PartialEq {
46    /// Compare two values of type `T` by the field's enum. Return values of `None` are treated as `NULL` values. See [`Sortable`] for more information.
47    ///
48    /// Be careful when comparing types like `Option` which implement `Ord`. This means that `None` and `Some` have an order where we might use them as unknown / `NULL` values. This can be a surprise.
49    ///
50    /// Another issue is `f64` only implements `PartialOrd` and not `Ord` because a value can hold `f64::NAN`. In this situation `partial_cmp` will return `None` and we'll treat these values as `NULL` as expected.
51    fn partial_cmp_by(&self, a: &T, b: &T) -> Option<Ordering>;
52}
53
54/// Trait used to describe how a field can be sorted. This must be implemented on the field enum.
55///
56/// Our [`PartialOrdBy`] fn may result in `None` values which we refer to as `NULL`. We borrow from SQL here to handle these values in a similar way to the [SQL ORDER BY clause](https://www.postgresql.org/docs/current/sql-select.html#SQL-ORDERBY). The PostgreSQL general form is `ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...]` where:
57/// - `expression` is the field being sorted.
58/// - `ASC` and `DESC` are the sort [`Direction`].
59/// - `USING operator` is implied by [`PartialOrdBy`].
60/// - `NULLS { FIRST | LAST }` corresponds to [`NullHandling`].
61/// Meaning you can sort by ascending or descending and optionally specify `NULL` ordering.
62pub trait Sortable: PartialEq {
63    /// Describes how this field can be sorted.
64    fn sort_by(&self) -> Option<SortBy>;
65
66    /// Describes how `NULL` values (when [`PartialOrdBy`] returns `None`) should be ordered when sorting. Either all at the start or the end.
67    ///
68    /// Provided implementation relies on the default (all at the end) and should be overridden if you want to change this generally or on a per-field basis.
69    fn null_handling(&self) -> NullHandling {
70        NullHandling::default()
71    }
72}
73
74/// Describes how a field should be sorted. Returned by [`Sortable::sort_by`].
75#[derive(Copy, Clone, Debug, PartialEq)]
76pub enum SortBy {
77    /// This field is limited to being sorted in the one direction specified.
78    Fixed(Direction),
79    /// This field can be sorted in either direction. The direction specifies the initial direction. Fields of this sort can be toggled between directions.
80    Reversible(Direction),
81}
82
83/// Sort direction. Does not have a default -- implied by the field via [`SortBy`].
84///
85/// Actual sorting is done by [`PartialOrdBy`].
86#[derive(Copy, Clone, Debug, PartialEq)]
87pub enum Direction {
88    /// Ascending sort. A-Z, 0-9, little to big, etc.
89    Ascending,
90    /// Descending sort. Z-A, opposite of ascending.
91    Descending,
92}
93
94impl Direction {
95    /// Inverts the direction.
96    pub fn invert(&self) -> Self {
97        match self {
98            Self::Ascending => Self::Descending,
99            Self::Descending => Self::Ascending,
100        }
101    }
102
103    fn from_field<F: Sortable>(field: &F) -> Direction {
104        field.sort_by().unwrap_or_default().direction()
105    }
106}
107
108/// Describes how `NULL` values should be ordered when sorting. We refer to `None` values returned from [`PartialOrdBy::partial_cmp_by`] as `NULL`. Warning: Rust's `Option::None` is not strictly equivalent to SQL's `NULL` but we borrow from SQL terminology to handle them.
109#[derive(Copy, Clone, Debug, Default, PartialEq)]
110pub enum NullHandling {
111    /// Places all `NULL` values first.
112    First,
113    /// Places all `NULL` values last. The default.
114    #[default]
115    Last,
116}
117
118impl Default for SortBy {
119    fn default() -> SortBy {
120        Self::increasing_or_decreasing().unwrap()
121    }
122}
123
124impl SortBy {
125    /// Field may not be sorted. Convenience fn for specifying how a field may be sorted.
126    pub fn unsortable() -> Option<Self> {
127        None
128    }
129    /// Field may only be sorted in ascending order.
130    pub fn increasing() -> Option<Self> {
131        Some(Self::Fixed(Direction::Ascending))
132    }
133    /// Field may only be sorted in descending order.
134    pub fn decreasing() -> Option<Self> {
135        Some(Self::Fixed(Direction::Descending))
136    }
137    /// Field may be sorted in either direction. The initial direction is ascending. This is the default.
138    pub fn increasing_or_decreasing() -> Option<Self> {
139        Some(Self::Reversible(Direction::Ascending))
140    }
141    /// Field may be sorted in either direction. The initial direction is descending.
142    pub fn decreasing_or_increasing() -> Option<Self> {
143        Some(Self::Reversible(Direction::Descending))
144    }
145
146    /// Returns the initial / implied direction of the sort.
147    pub fn direction(&self) -> Direction {
148        match self {
149            Self::Fixed(dir) => *dir,
150            Self::Reversible(dir) => *dir,
151        }
152    }
153
154    fn ensure_direction(&self, dir: Direction) -> Direction {
155        use SortBy::*;
156        match self {
157            // Must match allowed
158            Fixed(allowed) if *allowed == dir => dir,
159            // Did not match allowed
160            Fixed(allowed) => *allowed,
161            // Any allowed
162            Reversible(_) => dir,
163        }
164    }
165}
166
167/// Builder for [UseSorter](UseSorter). Use this to specify the field and direction of the sorter. For example by passing sort state from URL parameters.
168///
169/// Ordering of [`Self::with_field`] and [`Self::with_direction`] matters as the builder will ignore invalid combinations specified by the field's [`Sortable`]. This is to prevent the user from specifying a direction that is not allowed by the field.
170#[derive(Copy, Clone, Debug, PartialEq)]
171pub struct UseSorterBuilder<F> {
172    field: F,
173    direction: Direction,
174}
175
176impl<F: Default + Sortable> Default for UseSorterBuilder<F> {
177    fn default() -> Self {
178        let field = F::default();
179        let direction = Direction::from_field(&field);
180        Self { field, direction }
181    }
182}
183
184impl<F: Copy + Default + Sortable> UseSorterBuilder<F> {
185    /// Optionally sets the initial field to sort by.
186    pub fn with_field(&self, field: F) -> Self {
187        Self { field, ..*self }
188    }
189
190    /// Optionally sets the initial direction to sort by.[`Direction::Ascending`] can be set.
191    pub fn with_direction(&self, direction: Direction) -> Self {
192        Self { direction, ..*self }
193    }
194
195    /// Creates Dioxus hooks to manage state. Must follow Dioxus hook rules and be called unconditionally in the same order as other hooks. See [use_sorter()] for simple usage.
196    ///
197    /// This fn (or [`Self::use_sorter`]) *must* be called or never used. See the docs on [`UseSorter::sort`] on using conditions.
198    ///
199    /// If the field or direction has not been set then the default values will be used.
200    pub fn use_sorter(self, cx: &ScopeState) -> UseSorter<F> {
201        let sorter = use_sorter(cx);
202        sorter.set_field(self.field, self.direction);
203        sorter
204    }
205}
206
207/// Creates Dioxus hooks to manage state. Must follow Dioxus hook rules and be called unconditionally in the same order as other hooks. See [UseSorterBuilder](UseSorterBuilder) for more advanced usage.
208///
209/// This fn (or [`UseSorterBuilder::use_sorter`]) *must* be called or never used. See the docs on [`UseSorter::sort`] on using conditions.
210///
211/// Relies on `F::default()` for the initial value.
212pub fn use_sorter<F: Copy + Default + Sortable>(cx: &ScopeState) -> UseSorter<'_, F> {
213    let field = F::default();
214    UseSorter {
215        field: use_state(cx, || field),
216        direction: use_state(cx, || Direction::from_field(&field)),
217    }
218}
219
220impl<'a, F> UseSorter<'a, F> {
221    /// Returns the current field and direction. Can be used to recreate state with [UseSorterBuilder](UseSorterBuilder).
222    pub fn get_state(&self) -> (&F, &Direction) {
223        (self.field.get(), self.direction.get())
224    }
225
226    /// Sets the sort field and toggles the direction (if applicable). Ignores unsortable fields.
227    pub fn toggle_field(&self, field: F)
228    where
229        F: Sortable,
230    {
231        match field.sort_by() {
232            None => (), // Do nothing, don't switch to unsortable
233            Some(sort_by) => {
234                use SortBy::*;
235                match sort_by {
236                    Fixed(dir) => self.direction.set(dir),
237                    Reversible(dir) => {
238                        // Invert direction if the same field
239                        let dir = if *self.field.get() == field {
240                            self.direction.get().invert()
241                        } else {
242                            // Reset state to new field
243                            dir
244                        };
245                        self.direction.set(dir);
246                    }
247                }
248                self.field.set(field);
249            }
250        }
251    }
252
253    /// Sets the sort field and direction state directly. Ignores unsortable fields. Ignores the direction if not valid for a field.
254    pub fn set_field(&self, field: F, dir: Direction)
255    where
256        F: Sortable,
257    {
258        match field.sort_by() {
259            None => (), // Do nothing, ignore unsortable
260            Some(sort_by) => {
261                // Set state but ensure direction is valid
262                let dir = sort_by.ensure_direction(dir);
263                self.field.set(field);
264                self.direction.set(dir);
265            }
266        }
267    }
268
269    /// Sorts items according to the current field and direction.
270    ///
271    /// This is not a hook and may be called conditionally. For example:
272    /// - If data is coming from a `use_future` then you can call this fn once it has completed.
273    /// - If you need to apply a filter, do so before calling this fn.
274    pub fn sort<T>(&self, items: &mut [T])
275    where
276        F: PartialOrdBy<T> + Sortable,
277    {
278        let (field, dir) = self.get_state();
279        sort_by(field, *dir, field.null_handling(), items);
280    }
281}
282
283fn sort_by<T, F: PartialOrdBy<T>>(
284    sort_by: &F,
285    dir: Direction,
286    nulls: NullHandling,
287    items: &mut [T],
288) {
289    items.sort_by(|a, b| {
290        let partial = sort_by.partial_cmp_by(a, b);
291        partial.map_or_else(
292            || {
293                let a_is_null = sort_by.partial_cmp_by(a, a).is_none();
294                let b_is_null = sort_by.partial_cmp_by(b, b).is_none();
295                match (a_is_null, b_is_null) {
296                    (true, true) => Ordering::Equal,
297                    (true, false) => match nulls {
298                        NullHandling::First => Ordering::Less,
299                        NullHandling::Last => Ordering::Greater,
300                    },
301                    (false, true) => match nulls {
302                        NullHandling::First => Ordering::Greater,
303                        NullHandling::Last => Ordering::Less,
304                    },
305                    // Uh-oh, first partial_cmp_by should not have returned None
306                    (false, false) => unreachable!(),
307                }
308            },
309            // Reversal must be applied per item to avoid ordering NULLs
310            |o| match dir {
311                Direction::Ascending => o,
312                Direction::Descending => o.reverse(),
313            },
314        )
315    });
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[derive(Clone, Debug, Default, PartialEq)]
323    struct Row(f64);
324
325    #[derive(Copy, Clone, Debug, Default, PartialEq)]
326    enum RowField {
327        #[default]
328        Value,
329    }
330
331    impl PartialOrdBy<Row> for RowField {
332        fn partial_cmp_by(&self, a: &Row, b: &Row) -> Option<Ordering> {
333            match self {
334                Self::Value => a.0.partial_cmp(&b.0),
335            }
336        }
337    }
338
339    #[test]
340    fn test_sort_by() {
341        use Direction::*;
342        use NullHandling::*;
343        use RowField::*;
344
345        // Ascending
346        let mut rows = vec![Row(2.0), Row(1.0), Row(3.0)];
347        sort_by(&Value, Ascending, First, rows.as_mut_slice());
348        assert_eq!(rows, vec![Row(1.0), Row(2.0), Row(3.0)]);
349        // Descending
350        sort_by(&Value, Descending, First, rows.as_mut_slice());
351        assert_eq!(rows, vec![Row(3.0), Row(2.0), Row(1.0)]);
352
353        // Nulls last, ascending
354        let mut rows = vec![Row(f64::NAN), Row(f64::NAN), Row(2.0), Row(1.0), Row(3.0)];
355        sort_by(&Value, Ascending, Last, rows.as_mut_slice());
356        assert_eq!(rows[0], Row(1.0));
357        assert_eq!(rows[1], Row(2.0));
358        assert_eq!(rows[2], Row(3.0));
359        assert!(rows[3].0.is_nan());
360        assert!(rows[4].0.is_nan());
361        // Nulls first, ascending
362        sort_by(&Value, Ascending, First, rows.as_mut_slice());
363        assert!(rows[0].0.is_nan());
364        assert!(rows[1].0.is_nan());
365        assert_eq!(rows[2], Row(1.0));
366        assert_eq!(rows[3], Row(2.0));
367        assert_eq!(rows[4], Row(3.0));
368
369        // Nulls last, descending
370        sort_by(&Value, Descending, Last, rows.as_mut_slice());
371        assert_eq!(rows[0], Row(3.0));
372        assert_eq!(rows[1], Row(2.0));
373        assert_eq!(rows[2], Row(1.0));
374        assert!(rows[3].0.is_nan());
375        assert!(rows[4].0.is_nan());
376        // Nulls first, descending
377        sort_by(&Value, Descending, First, rows.as_mut_slice());
378        assert!(rows[0].0.is_nan());
379        assert!(rows[1].0.is_nan());
380        assert_eq!(rows[2], Row(3.0));
381        assert_eq!(rows[3], Row(2.0));
382        assert_eq!(rows[4], Row(1.0));
383    }
384}