Rust の構造体Vecをsort_byしやすくするマクロを書いた

なんでつくったか

例えば x, y, z がある以下のような構造体があって、

struct Sample {
    x: i64,
    y: i64,
    z: i64,
}

v: Vec<Sample>をソートしたい。ただし、並び替える順番は利用箇所によって違う。

こんなときに、いつも、 sort_by を使って、以下のように書いていた*1

// x昇順→y昇順→z昇順
v.sort_by(|l, r| {
    match l.x.cmp(&r.x) {
        std::cmp::Ordering::Equal => {
            match l.y.cmp(&r.y) {
                std::cmp::Ordering::Equal => {
                    l.z.cmp(&r.z)
                },
                other => other
            }
        },
        other => other
    }
});

これは書くの面倒だし、やりたいことの割に長くて目を引く。

ところでSQLでは order by x, y, z と書けるな、とふと思って、 そんな感じで簡単に書けるようにするマクロが欲しくなった。ので作った。

使い方

以下のように使う。

let mut v = vec![
    Sample {x: 10, y: 200, z:3000},
    Sample {x: 10, y: 100, z:1000},
    // ...
];

// x昇順→y昇順→z昇順
v.sort_by(order_by!(x, y, z));
// x昇順→y降順
v.sort_by(order_by!(x asc, y desc));
// z昇順→x昇順
v.sort_by(order_by!(z asc, x));

さらに、タプルでもいける。

let mut v2: Vec<(i64, i64, i64)> = vec![
    (20, 100, 1000),
    (10, 300, 1000),
    (10, 300, 2000),
    (30, 300, 2000),
    (30, 300, 3000),
    // ...
];

v2.sort_by(order_by!(0, 1, 2));
v2.sort_by(order_by!(0 desc, 1, 2));
v2.sort_by(order_by!(2, 0, 1));

マクロコード

macro_rules! order_by {
    ($($x:tt)*) => {
        |l, r| {
            order_by_inner!(l, r, $($x)*)
        }
    }
}
macro_rules! order_by_inner {
    () => {};
    ($l:ident) => {std::cmp::Ordering::Equal};
    ($l:ident , ) => {std::cmp::Ordering::Equal};
    ($l:ident , $r:ident) => {std::cmp::Ordering::Equal};
    ($l:ident , $r:ident , ) => {std::cmp::Ordering::Equal};

    // last
    ($l:ident , $r:ident , $x:tt asc) => {
        $l.$x.cmp(&$r.$x)
    };
    ($l:ident , $r:ident , $x:tt desc) => {
        $l.$x.cmp(&$r.$x).reverse()
    };
    ($l:ident , $r:ident , $x:tt) => {
        order_by_inner!($l, $r, $x asc)
    };

    // mid
    ($l:ident , $r:ident , $x:tt asc , $($p:tt)+) => {
        match $l.$x.cmp(&$r.$x) {
            std::cmp::Ordering::Equal => {
                order_by_inner!($l, $r, $($p)+)
            },
            other => other
        }
    };
    ($l:ident , $r:ident , $x:tt desc , $($p:tt)+) => {
        match $l.$x.cmp(&$r.$x).reverse() {
            std::cmp::Ordering::Equal => {
                order_by_inner!($l, $r, $($p)+)
            },
            other => other
        }
    };
    ($l:ident , $r:ident , $x:tt , $($p:tt)+) => {
        order_by_inner!($l, $r, $x asc, $($p)+)
    };
}

*1:もっといいやり方があったら教えてほしい