Enum.sort_by/3で複雑な並び替え仕様を簡単に実装する（Elixir）

**つくばねの峰より落つるみなの川恋ぞ積りて淵となりぬる**

Advent Calendar 2022 62日目[^1]の記事です。
I'm looking forward to 12/25,2022 :santa::santa_tone1::santa_tone2::santa_tone3::santa_tone4::santa_tone5:
私の[Advent Calendar 2022 一覧](https://docs.google.com/spreadsheets/d/1HQvFjagQLRPjOYAjDVzWp9S4b8dKixxvvaz_TtbZWto/edit#gid=1723448955)。

[^1]: @kaizen_nagoya さんの「[「@e99h2121 アドベントカレンダーではありますまいか Advent Calendar 2020」の改訂版ではありますまいか Advent Calendar 2022 １日目　Most Breakthrough Generator](https://qiita.com/kaizen_nagoya/items/49ebebee3a0377f3b59b)」から着想を得て、模倣いたしました。 

---

# はじめに

[Elixir](https://elixir-lang.org/)を楽しんでいますか:bangbang::bangbang::bangbang:

今日は`.md`内のテーブルをソートしてみます。
[Enum.sort_by/3](https://hexdocs.pm/elixir/Enum.html#sort_by/3)を使います。

# 仕様

仕様は以下の通りとします。

- `README.md`の構造はかわらないものとする
    - 特にテーブルの列は変わらないものとする
- テーブルの行は増えることがある
- テーブルの並び順は基本的に以下の順番とする
    - Category順(UTF-8の文字列順)
    - Name順(UTF-8の文字列順)
    - Languageは`Japanese`を優先、他はUTF-8の文字列順
    - URL順(UTF-8の文字列順)
- ただし、Category=Etcはテーブルの後方に置く

並び替えの仕様が複雑です。

## サンプル

サンプルを示します。

### Input

InputのREADME.mdはこういうものです。

```md:README.md
# Awesome Twitter Communities for Engineers

[![Build Status](https://github.com/mattn/awesome-twitter-communities/actions/workflows/lint.yaml/badge.svg?branch=main)](https://github.com/mattn/awesome-twitter-communities/actions/workflows/lint.yaml?query=branch%3Amain)

|Name|Category|Language|URL|
|-|-|-|-|
|AWS|Cloud Infrastructure|English|<https://twitter.com/i/communities/1471503983839567878>|
|WebRTC|Communication technology|English|<https://twitter.com/i/communities/1498133315164860419>|
|Kubernetes|Container|English|<https://twitter.com/i/communities/1444745802383953921>|
|Kubernetes & コンテナ技術|Container|Japanese|<https://twitter.com/i/communities/1498974495989956614>|
|MySQL-JP|Database|Japanese|<https://twitter.com/i/communities/1496795585982382084>|
|NewSQL_jp|Database|Japanese|<https://twitter.com/i/communities/1498603867285581824>|
|自作DBMS友の会|Database|Japanese|<https://twitter.com/i/communities/1498114917672505344>|
|OSS 日本語 L10n|Localization|Japanese|<https://twitter.com/i/communities/1498961283546157061>|
|Linuxカーネル|OS|Japanese|<https://twitter.com/i/communities/1499543572189618176>|
|vyosjp|OS|Japanese|<https://twitter.com/i/communities/1498101007733370880>|
|.NET_Japan|Programming Language|Japanese|<https://twitter.com/i/communities/1496977662640009217>|
|C++|Programming Language|Japanese|<https://twitter.com/i/communities/1499396722514345984>|
|Deno-ja|Programming Language|Japanese|<https://twitter.com/i/communities/1498174921562013698>|
|F# on Twitter|Programming Language|English|<https://twitter.com/i/communities/1493280005589196801>|
|Java|Programming Language|Japanese|<https://twitter.com/i/communities/1497219281079398402>|
|Java|Programming Language|English|<https://twitter.com/i/communities/1471178821906821122>|
|Julia Programming Language|Programming Language|English|<https://twitter.com/i/communities/1441046367514755082>|
|Julia言語|Programming Language|Japanese|<https://twitter.com/i/communities/1499390501467811845>|
|PHP-ja|Programming Language|Japanese|<https://twitter.com/i/communities/1497741277789835264>|
|Pythonジャパン|Programming Language|Japanese|<https://twitter.com/i/communities/1498184748728205315>|
|The Go Programming Language|Programming Language|English|<https://twitter.com/i/communities/1493637136502960134>|
|TypeScript|Programming Language|Japanese|<https://twitter.com/i/communities/1499329858178289664>|
|WebAssembly|Programming Language|English|<https://twitter.com/i/communities/1497545442023944192>|
|clojure|Programming Language|English|<https://twitter.com/i/communities/1494013093059432451>|
|dotnet|Programming Language|English|<https://twitter.com/i/communities/1488624124817666051>|
|elixir|Programming Language|English|<https://twitter.com/i/communities/1493287155942232066>|
|elixir-jp|Programming Language|Japanese|<https://twitter.com/i/communities/1498232167864082435>|
|ruby-jp|Programming Language|Japanese|<https://twitter.com/i/communities/1496768365683408900>|
|サバイバルTypeScript|Programming Language|Japanese|<https://twitter.com/i/communities/1499723154956390401>|
|プログラミング言語Go|Programming Language|Japanese|<https://twitter.com/i/communities/1498095077222400000>|
|プログラミング言語Rust|Programming Language|Japanese|<https://twitter.com/i/communities/1498496039401451522>|
|プログラミング言語処理系|Programming Language|Japanese|<https://twitter.com/i/communities/1499381283864342530>|
|Security Engineering|Security|Japanese|<https://twitter.com/i/communities/1498138021723467781>|
|Webセキュリティ|Security|Japanese|<https://twitter.com/i/communities/1498053973097730048>|
|VS Codeの会|Text Editor|Japanese|<https://twitter.com/i/communities/1498865559353511941>|
|emacs-jp|Text Editor|Japanese|<https://twitter.com/i/communities/1498276712034947072>|
|vim-jp|Text Editor|Japanese|<https://twitter.com/i/communities/1497961032404594691>|
|Ebiten|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1498350105346600960>|
|Flutter|Tools/Libraries|English|<https://twitter.com/i/communities/1472249315724771329>|
|Jotai Friends JP|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1497150937806213120>|
|Nuxt|Tools/Libraries|English|<https://twitter.com/i/communities/1498235047194808320>|
|SATySFi|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1498074334619123712>|
|Spring Developers|Tools/Libraries|English|<https://twitter.com/i/communities/1496544801533091844>|
|Svelte日本|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1499182207491260424>|
|DATA Saber|Etc|Japanese|<https://twitter.com/i/communities/1498660583415361536>|
|IE11を偲ぶ会|Etc|Japanese|<https://twitter.com/i/communities/1498870842784043009>|
|ITエンジニアを褒める会|Etc|Japanese|<https://twitter.com/i/communities/1494319842585083906>|
|JMUG|Etc|Japanese|<https://twitter.com/i/communities/1498585537527320577>|
|Japan Okta User Group|Etc|Japanese|<https://twitter.com/i/communities/1498432461839036418>|
|SREとかObservabilityとか|Etc|Japanese|<https://twitter.com/i/communities/1498088713670172675>|
|Tech Twitter|Etc|English|<https://twitter.com/i/communities/1472105760389668865>|
|Tech Twitter - Japan|Etc|Japanese|<https://twitter.com/i/communities/1494649689215737856>|
|Twitter Developers Japan|Etc|Japanese|<https://twitter.com/i/communities/1493041080077795328>|
|UdonCreatorCommunity|Etc|Japanese|<https://twitter.com/i/communities/1497068375553765378>|
|nlp-jp|Etc|Japanese|<https://twitter.com/i/communities/1498287599928365062>|
|sudo-jp|Etc|Japanese|<https://twitter.com/i/communities/1499227044982374401>|
|ウェブパフォーマンス|Etc|Japanese|<https://twitter.com/i/communities/1498089387422515202>|
|ガジェット|Etc|Japanese|<https://twitter.com/i/communities/1498224086652121099>|
|バンクーバー日本人ソフトウェアエンジニア|Etc|Japanese|<https://twitter.com/i/communities/1498574247895056384>|
|吉祥寺.pm|Etc|Japanese|<https://twitter.com/i/communities/1498106494989967363>|
|日本のウェブフォームを良くする開発者の会|Etc|Japanese|<https://twitter.com/i/communities/1498168121336614916>|
|自作キーボード|Etc|Japanese|<https://twitter.com/i/communities/1495468692813287425>|
|鹿児島ITエンジニア|Etc|Japanese|<https://twitter.com/i/communities/1496763936682954752>|
|ネットワーク自動化|Etc|Japanese|<https://twitter.com/i/communities/1498540422154821633>|

```

### Output

これを正解とします。

```md:README.md
# Awesome Twitter Communities for Engineers

[![Build Status](https://github.com/mattn/awesome-twitter-communities/actions/workflows/lint.yaml/badge.svg?branch=main)](https://github.com/mattn/awesome-twitter-communities/actions/workflows/lint.yaml?query=branch%3Amain)

|Name|Category|Language|URL|
|-|-|-|-|
|AWS|Cloud Infrastructure|English|<https://twitter.com/i/communities/1471503983839567878>|
|WebRTC|Communication technology|English|<https://twitter.com/i/communities/1498133315164860419>|
|Kubernetes|Container|English|<https://twitter.com/i/communities/1444745802383953921>|
|Kubernetes & コンテナ技術|Container|Japanese|<https://twitter.com/i/communities/1498974495989956614>|
|MySQL-JP|Database|Japanese|<https://twitter.com/i/communities/1496795585982382084>|
|NewSQL_jp|Database|Japanese|<https://twitter.com/i/communities/1498603867285581824>|
|自作DBMS友の会|Database|Japanese|<https://twitter.com/i/communities/1498114917672505344>|
|OSS 日本語 L10n|Localization|Japanese|<https://twitter.com/i/communities/1498961283546157061>|
|Linuxカーネル|OS|Japanese|<https://twitter.com/i/communities/1499543572189618176>|
|vyosjp|OS|Japanese|<https://twitter.com/i/communities/1498101007733370880>|
|.NET_Japan|Programming Language|Japanese|<https://twitter.com/i/communities/1496977662640009217>|
|C++|Programming Language|Japanese|<https://twitter.com/i/communities/1499396722514345984>|
|Deno-ja|Programming Language|Japanese|<https://twitter.com/i/communities/1498174921562013698>|
|F# on Twitter|Programming Language|English|<https://twitter.com/i/communities/1493280005589196801>|
|Java|Programming Language|Japanese|<https://twitter.com/i/communities/1497219281079398402>|
|Java|Programming Language|English|<https://twitter.com/i/communities/1471178821906821122>|
|Julia Programming Language|Programming Language|English|<https://twitter.com/i/communities/1441046367514755082>|
|Julia言語|Programming Language|Japanese|<https://twitter.com/i/communities/1499390501467811845>|
|PHP-ja|Programming Language|Japanese|<https://twitter.com/i/communities/1497741277789835264>|
|Pythonジャパン|Programming Language|Japanese|<https://twitter.com/i/communities/1498184748728205315>|
|The Go Programming Language|Programming Language|English|<https://twitter.com/i/communities/1493637136502960134>|
|TypeScript|Programming Language|Japanese|<https://twitter.com/i/communities/1499329858178289664>|
|WebAssembly|Programming Language|English|<https://twitter.com/i/communities/1497545442023944192>|
|clojure|Programming Language|English|<https://twitter.com/i/communities/1494013093059432451>|
|dotnet|Programming Language|English|<https://twitter.com/i/communities/1488624124817666051>|
|elixir|Programming Language|English|<https://twitter.com/i/communities/1493287155942232066>|
|elixir-jp|Programming Language|Japanese|<https://twitter.com/i/communities/1498232167864082435>|
|ruby-jp|Programming Language|Japanese|<https://twitter.com/i/communities/1496768365683408900>|
|サバイバルTypeScript|Programming Language|Japanese|<https://twitter.com/i/communities/1499723154956390401>|
|プログラミング言語Go|Programming Language|Japanese|<https://twitter.com/i/communities/1498095077222400000>|
|プログラミング言語Rust|Programming Language|Japanese|<https://twitter.com/i/communities/1498496039401451522>|
|プログラミング言語処理系|Programming Language|Japanese|<https://twitter.com/i/communities/1499381283864342530>|
|Security Engineering|Security|Japanese|<https://twitter.com/i/communities/1498138021723467781>|
|Webセキュリティ|Security|Japanese|<https://twitter.com/i/communities/1498053973097730048>|
|VS Codeの会|Text Editor|Japanese|<https://twitter.com/i/communities/1498865559353511941>|
|emacs-jp|Text Editor|Japanese|<https://twitter.com/i/communities/1498276712034947072>|
|vim-jp|Text Editor|Japanese|<https://twitter.com/i/communities/1497961032404594691>|
|Ebiten|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1498350105346600960>|
|Flutter|Tools/Libraries|English|<https://twitter.com/i/communities/1472249315724771329>|
|Jotai Friends JP|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1497150937806213120>|
|Nuxt|Tools/Libraries|English|<https://twitter.com/i/communities/1498235047194808320>|
|SATySFi|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1498074334619123712>|
|Spring Developers|Tools/Libraries|English|<https://twitter.com/i/communities/1496544801533091844>|
|Svelte日本|Tools/Libraries|Japanese|<https://twitter.com/i/communities/1499182207491260424>|
|DATA Saber|Etc|Japanese|<https://twitter.com/i/communities/1498660583415361536>|
|IE11を偲ぶ会|Etc|Japanese|<https://twitter.com/i/communities/1498870842784043009>|
|ITエンジニアを褒める会|Etc|Japanese|<https://twitter.com/i/communities/1494319842585083906>|
|JMUG|Etc|Japanese|<https://twitter.com/i/communities/1498585537527320577>|
|Japan Okta User Group|Etc|Japanese|<https://twitter.com/i/communities/1498432461839036418>|
|SREとかObservabilityとか|Etc|Japanese|<https://twitter.com/i/communities/1498088713670172675>|
|Tech Twitter|Etc|English|<https://twitter.com/i/communities/1472105760389668865>|
|Tech Twitter - Japan|Etc|Japanese|<https://twitter.com/i/communities/1494649689215737856>|
|Twitter Developers Japan|Etc|Japanese|<https://twitter.com/i/communities/1493041080077795328>|
|UdonCreatorCommunity|Etc|Japanese|<https://twitter.com/i/communities/1497068375553765378>|
|nlp-jp|Etc|Japanese|<https://twitter.com/i/communities/1498287599928365062>|
|sudo-jp|Etc|Japanese|<https://twitter.com/i/communities/1499227044982374401>|
|ウェブパフォーマンス|Etc|Japanese|<https://twitter.com/i/communities/1498089387422515202>|
|ガジェット|Etc|Japanese|<https://twitter.com/i/communities/1498224086652121099>|
|ネットワーク自動化|Etc|Japanese|<https://twitter.com/i/communities/1498540422154821633>|
|バンクーバー日本人ソフトウェアエンジニア|Etc|Japanese|<https://twitter.com/i/communities/1498574247895056384>|
|吉祥寺.pm|Etc|Japanese|<https://twitter.com/i/communities/1498106494989967363>|
|日本のウェブフォームを良くする開発者の会|Etc|Japanese|<https://twitter.com/i/communities/1498168121336614916>|
|自作キーボード|Etc|Japanese|<https://twitter.com/i/communities/1495468692813287425>|
|鹿児島ITエンジニア|Etc|Japanese|<https://twitter.com/i/communities/1496763936682954752>|

```

# Elixirのプログラム

```elixir:sort.exs
lines =
  File.read!("README.md")
  |> String.split("\n")

fixed_contents_cnt =
  lines
  |> Enum.find_index(& &1 == "|-|-|-|-|")
  |> Kernel.+(1)

table_contents =
  lines
  |> Enum.drop(fixed_contents_cnt)
  |> Enum.reject(& &1 == "")

mapper = fn name, category, language, url ->
  language = if language == "Japanese", do: "\u0000", else: language

  if category == "Etc" do
    {category, name, language, url, :etc}
  else
    {category, name, language, url}
  end
end

sorted_table_contents =
  table_contents
  |> Enum.map(& {&1, String.split(&1, "|")})
  |> Enum.sort_by(fn {_, ["", name, category, language, url, ""]} ->
    mapper.(name, category, language, url)
  end)
  |> Enum.map(& elem(&1, 0))

Enum.take(lines, fixed_contents_cnt)
|> Kernel.++(sorted_table_contents)
|> Kernel.++([""])
|> Enum.join("\n")
|> then(&File.write!("README.md", &1))
```

```Dockerfile:Dockerfile
FROM elixir:1.13.3-slim

WORKDIR /app

COPY . .

CMD ["elixir", "sort.exs"]
```

## 解説

少し解説をします。
Dockerfileは、[Elixir](https://elixir-lang.org/)を未インストールでも動かせるように用意しました。
[Elixir](https://elixir-lang.org/)をインストール済の方は気にしないでください。

このプログラムの肝はソートです。
[Enum.sort_by/3](https://hexdocs.pm/elixir/Enum.html#sort_by/3)を使いました。
この関数に指定する`mapper`関数が肝です。
`mapper`関数は、各要素どうしを何で比較するのかを返します。

もう一度仕様を記載します。

- テーブルの並び順は基本的に以下の順番とする
    - Category順(UTF-8の文字列順)
    - Name順(UTF-8の文字列順)
    - Languageは`Japanese`を優先、他はUTF-8の文字列順
    - URL順(UTF-8の文字列順)
- ただし、Category=Etcはテーブルの後方に置く


タプルを使って、比較したい順に要素を並べればよいです。

```elixir
mapper = fn name, category, language, url ->
  language = if language == "Japanese", do: "\u0000", else: language

  if category == "Etc" do
    {category, name, language, url, :etc}
  else
    {category, name, language, url}
  end
end
```

例外部分については以下のように細工をしました。
「Languageは`Japanese`を優先、他はUTF-8の文字列順」については、`Japanese`の場合にはNULL文字だけの文字列で置き換えることとしました。
NULL文字だけの文字列は、[Elixir](https://elixir-lang.org/)は以下のように書きます。

- `"\u0000"` もしくは
- `<<0>>`

(追記) 単純に空文字列 `""` でよいです。


「Category=Etcはテーブルの後方に置く」についてはタプルの要素を増やしておくことで後ろにまわすことにしました。

[Elixir](https://elixir-lang.org/)は、ソートをすっきり書けます。


## 動かし方

Inputの`README.md`を同じフォルダに置いておいてください。

```bash
docker build -t awesome-twitter-communities-sort .                
docker run --rm -v "$PWD":/app awesome-twitter-communities-sort
```

もちろん[Elixir](https://elixir-lang.org/)をインストール済の場合は以下のように実行できます。

```bash
elixir sort.exs
```

# mattn/awesome-twitter-communities

[Twitterのコミュニティ](https://help.twitter.com/ja/using-twitter/communities)のうち、技術系のコミュニティを @mattn さんがまとめてくださっています。
ありがとうございます！

https://github.com/mattn/awesome-twitter-communities

たまに @mattn さんがソートコミットを入れられていたので、[Elixir](https://elixir-lang.org/)で書いてみたらどうなるのだろう？　とおもって書いてみました。
きっとすでにツールはお持ちだったり、GitHub Actionsに自動ソートが組み込まれていいるのじゃないかとおもったりもしました。
せっかく作ったので、プルリクエストを送ってみました。
rejectされてもいいや〜　という気持ちです。
その旨、コメントに `I don't mind` と書きました。

https://github.com/mattn/awesome-twitter-communities/pull/32

プルリクエストを送ったあとにすでに、@mattn さんのほうでGoのプログラムが作られていることに気づきました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">awesome-twitter-communities の並びを直すのが面倒になってきたのでツールをこさえた。<a href="https://t.co/I2BUdTiWcA">https://t.co/I2BUdTiWcA</a></p>&mdash; mattn (@mattn_jp) <a href="https://twitter.com/mattn_jp/status/1499612195306733569?ref_src=twsrc%5Etfw">March 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

それで、やっぱりプルリクエストはrejectでした :sweat_smile: 
けれでも、[Enum.sort_by/3](https://hexdocs.pm/elixir/Enum.html#sort_by/3)に詳しくなれたし、[Elixir](https://elixir-lang.org/)は文字列操作がしやすいなあと感じられたので良かったです。
何よりも私は、[Elixir](https://elixir-lang.org/)を楽しめました。

何気に順番も大事で、
https://github.com/mattn/sort-awesome-twitter-communities/ リポジトリを先に見つけていたら[Elixir](https://elixir-lang.org/)で作ることはしなかったかもしれません。

今回はソートがポイントだったのでやはり
<font color="purple">$\huge{順番は大事💜}$</font>
です。

---

[mattn/awesome-twitter-communities](https://github.com/mattn/awesome-twitter-communities)にプルリクエストを送る際には、@mattn さんの[sort-awesome-twitter-communities/](https://github.com/mattn/sort-awesome-twitter-communities/)でテーブルの行の並び替えを
<font color="purple">$\huge{そ〜っと💜}$</font>
しておくと良いとおもいます。

## ソート

[mattn/awesome-twitter-communities](https://github.com/mattn/awesome-twitter-communities)に`format-readme.cmd` と `format-readme.sh`が追加されていました。
macOSで試しました。

```
./format-readme.sh
```

2022/03/05 12:12 [adc0682](https://github.com/mattn/awesome-twitter-communities/commit/adc068221582ed82247a5b731a76bbe9b8062c24)では、`Java`のところで差分がでました。
Languageのところを[adc0682](https://github.com/mattn/awesome-twitter-communities/commit/adc068221582ed82247a5b731a76bbe9b8062c24)ではJapaneseが前にきていますが、スクリプトを実行するとEnglishが前にきます。
これはこんなところで言う話ではなくて、GitHubで言う話でしょうが、正解は作者の方の最新が正解ですので、特にアクションは起こしません。

この記事は架空の`README.md`を題材とした話で、Languageは`Japanese`を優先ということをそのままにしておきます。
そのほうがプログラムに妙味がでるので。

---

```
git clone https://github.com/mattn/sort-awesome-twitter-communities
cd sort-awesome-twitter-communities
```

`README.md`を同じフォルダに置いておいて

```
go main.go
```



# Wrapping up :lgtm::lgtm::lgtm::lgtm::lgtm:

Enjoy [Elixir](https://elixir-lang.org/):bangbang::bangbang::bangbang:
<font color="purple">$\huge{Enjoy\ Elixir🚀}$</font>

今回は、[Enum.sort_by/3](https://hexdocs.pm/elixir/Enum.html#sort_by/3)に指定する`mapper`関数にて、並び替えたい優先度順に要素を並べておくだけで、複雑な並び替え仕様を簡単に実装できることを示しました。

[mattn/awesome-twitter-communities](https://github.com/mattn/awesome-twitter-communities)にプルリクエストを送る際には、@mattn さんの[sort-awesome-twitter-communities/](https://github.com/mattn/sort-awesome-twitter-communities/)でテーブルの行の並び替えを
<font color="purple">$\huge{そ〜っと💜}$</font>
しておくと良いとおもいます。
`format-readme.sh` or `format-readme.cmd`の実行を〜!!!

以上です。