Sort::Naturally(3pm)

πŸ“¦
version 1.03 Β· 7 versions
, in debian bookworm, debian trixie, ubuntu noble, ubuntu questing, ubuntu resolute, ubuntu stonking

πŸ’– NAME πŸ’–

okay so this is the page for Sort::Naturally (Sort::Naturally(3pm)), from the User Contributed Perl Documentation crew πŸŽ€. It's a sort module that sorts the words lexically but sorts the number bits numerically, so foo9 doesn't get stuck after foo100 😭

Sort::Naturally -- sort lexically, but sort numeral parts numerically ✨

🌸 SYNOPSIS 🌸

@them = nsort(qw(
 foo12a foo12z foo13a foo 14 9x foo12 fooa foolio Foolio Foo12a
));
print join(' ', @them), "\n";

Prints:

9x 14 foo fooa foolio Foolio foo12 foo12a Foo12a foo12z foo13a

(Or foo12a + Foo12a and foolio + Foolio might be switched, depending on your locale. it's giving locale drama πŸŒπŸ’…)

πŸ¦‹ DESCRIPTION πŸ¦‹

okay bestie, this module exports two functions, nsort and ncmp. they're how I built my own version of "natural sorting" 🧚 In natural sorting, numeric chunks get compared as numbers, and everything else that's made of word characters gets compared as text.

This is the vibe I'm going for when I say natural sorting:

  • Non-numeric word-character substrings are sorted as text, and it doesn't care about upper or lower case. so Foo lands between fish and fowl ✨
  • Numeric substrings are sorted as numbers, so 100 comes after 20 and not before it πŸ”’
  • \W substrings (neither word characters nor digits) get ignored completely πŸ™ˆ
  • Locale matters here. \w, \d, \D, and \W all depend on your locale, because Sort::Naturally uses a use locale statement 🌍
  • When two strings are compared and a number in one isn't lined up against a number in the other, the non-number always goes first. To make that work, a missing number gets treated as the value -1, like this:
foo       =>  "foo",  -1
foobar    =>  "foo",  -1,  "bar"
foo13     =>  "foo",  13,
foo13xyz  =>  "foo",  13,  "xyz"

That's how foo stays in front of foo13, and foo13 stays in front of foobar πŸ’…

  • The start of a string is the exception. Leading non-\W pieces (not word characters and not digits) get ignored, and numbers come before letters there πŸ‘‘
  • What counts as a "numeric substring"? Just runs that match m/\d+/. Scientific notation, commas, decimals, and all that jazz don't get detected. If your data has thousands separators like "20,000 Leagues Under The Sea" or "20.000 lieues sous les mers", strip them out before you feed things into nsort or ncmp 🫢

✨ The nsort function ✨

This function takes a list of strings and gives you back a sorted copy of that list.

This is the one most people will want:

@stuff = nsort(...list...);

When nsort needs to compare non-numeric substrings, it uses Perl's lc function inside a <use locale> scope. And when nsort needs to lowercase stuff, it also uses Perl's lc function inside a <use locale> scope. If you'd rather nsort used other functions, you can pass them in an arrayref as the first argument to nsort:

@stuff = nsort( [
                  \&string_comparator,   # optional
                  \&lowercaser_function  # optional
                ],
                ...list...
              );

If you only want a custom string comparator and no lowercaser, your options list is [\&comparator, ''] or [\&comparator]. If you want a lowercaser but no custom comparator, your options list is ['', \&lowercaser].

Any comparator you pass in gets called as $comparator->($left, $right). Just like a normal Perl cmp replacement, it has to return -1, 0, or 1, depending on whether the left argument is stringwise less than, equal to, or greater than the right one 🧠

Any lowercaser you pass in gets called as $lowercased = $lowercaser->($original).

⚠️ bestie, the lowercaser must not modify its $_[0]. Don't touch the original, it's not worth the drama πŸ’€

πŸŽ€ The ncmp function πŸŽ€

Sometimes you're sorting non-string values, like this:

@objects_sorted = sort { $a->tag cmp $b->tag } @objects;

...or maybe you're doing a Schwartzian transform, like this:

@strings =
  map $_->[0]
  sort { $a->[1] cmp $b->[1] }
  map { [$_, make_a_sort_key_from($_) ]
  @_
;

...and in those cases you might want something that replaces cmp, not sort. That's what the ncmp function from Sort::Naturally is for. You call it as ncmp($left,$right) instead of $left cmp $right, and otherwise it's a totally fine swap-in:

@objects_sorted = sort { ncmp($a->tag,$b->tag) } @objects;

@strings =
  map $_->[0]
  sort { ncmp($a->[1], $b->[1]) }
  map { [$_, make_a_sort_key_from($_) ]
  @_
;

Just like nsort, ncmp can take a different string comparator and/or lowercaser. Pass an arrayref as the first argument:

ncmp( [
        \&string_comparator,   # optional
        \&lowercaser_function  # optional
      ],
      $left, $right
    )

You might be able to get string comparators from Sort::ArbBiLex πŸ¦‹

πŸ“ NOTES πŸ“

  • This module is not a substitute for Sort::Versions! If you just need proper version sorting, go use that, bestie πŸ’…
  • If you need something that kinda works like this module's functions but not quite the same, consider scrolling through this module's source code and adapting what you find. Past the POD, there are several alternate attempts I made at natural sorting routines. They're not compiled as part of the module, but you might find them useful. They should all be working implementations of slightly different algorithms (all based on Martin Pool's "nsort"), which I eventually ditched in favor of my own algorithm. If you're naturally sorting very large data sets and sorting is getting ridiculously slow, you could try one of those discarded functions. I have a feeling they might be faster on large data sets. Benchmark them on your data and see πŸ“Š (Unless you really need the speed, don't bother. Hint: swap sort in for nsort in your code. If your program doesn't speed up drastically, the sorting isn't the problem. But if it is nsort slowing things down, consider something like this:)
      if(@set >= SOME_VERY_BIG_NUMBER) {
        no locale; # vroom vroom
        @sorted = sort(@set);  # feh, good enough
      } elsif(@set >= SOME_BIG_NUMBER) {
        use locale;
        @sorted = sort(@set);  # feh, good enough
      } else {
        # but keep it pretty for normal cases
        @sorted = nsort(@set);
      }
  • If you do adapt the routines in this module, email me! I'd genuinely love to hear about it πŸ₯Ή
  • Thanks to the EFNet #perl people for encouraging this module, especially magister and a-mused 🫢

Copyright 2001, Sean M. Burke sburke@cpan.org, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.

πŸ‘‘ AUTHOR πŸ‘‘

Sean M. Burke sburke@cpan.org πŸ’œ


okay that's the whole tea πŸ“ this page came from perl v5.36.0, last updated 2022-11-19, so go be a sorting legend, bestie πŸ§šβœ¨πŸ¦‹

πŸ“¦ where this page comes from