macOSのソートがアホで困る

これは SmartHR Advent Calendar 2024 シリーズ2 の2日目の記事です。

qiita.com

ちょっと前にツイートしたのがまあまあ見られたみたいなので、ちゃんと書いてみる(汚い言葉は良くないね)。

macOS で次のテキストファイルを sort コマンドでソートするとデタラメに並ぶんですよ。

千葉県千葉市
千葉県市川市
千葉県柏市
東京都北区
東京都千代田区
東京都大田区
東京都江戸川区
東京都江東区
東京都港区
$ sort a.txt
千葉県柏市
東京都北区
東京都港区
千葉県千葉市
千葉県市川市
東京都大田区
東京都江東区
東京都千代田区
東京都江戸川区

文字のコードを無視して文字数だけで並んでるように見える。使い物にならない。macOS アホすぎる。

原因は /usr/share/locale/ja_JP.UTF-8/LC_COLLATE ファイルが la_LN.US-ASCII/LC_COLLATE にリンクされてるからっぽい。

$ cd /usr/share/locale/
$ ls -l ja_JP.UTF-8/LC_COLLATE 
lrwxr-xr-x 1 root wheel 28 10 15 20:22 ja_JP.UTF-8/LC_COLLATE -> ../la_LN.US-ASCII/LC_COLLATE

blog.zhimingwang.org

ja.stackoverflow.com

LC_COLLATE は文字の照合規則を決めるもので、それが ASCII 文字のためのファイルを指してるということで、ASCII 文字以外は全部同値としてみなしてるということらしい。

つまりこういうデータをソートしてるのと同じ。そりゃ文字長順に並ぶわな。同じ文字とみなされた場合は文字コードの順に並ぶみたいだけども。

������
������
�����
�����
�������
������
�������
������
�����

なお LC_COLLATE=C を指定することで単純なバイト列としてソートされるので、文字コード順にソートされる。

$ LC_COLLATE=C sort a.txt
千葉県千葉市
千葉県市川市
千葉県柏市
東京都北区
東京都千代田区
東京都大田区
東京都江戸川区
東京都江東区
東京都港区

もともとこれに気がついたのは sort コマンドではなくて PostgreSQL の ORDER BY でのソートがおかしくなったからだった。

会社の開発用の PC は MacBook なんだけど、PostgreSQL は Docker を使って Linux 上で動かしてる。そのせいでかなり遅いんで、Docker 使わずに macOS 上で直接動かそうとしたらソート順がおかしくなった。

PostgreSQL のロケールは何もしないと libc が使われるので、OS 環境に依存する。

$ initdb -U postgres /tmp/hoge
postgres=# \x
拡張表示は on です。
postgres=# \l postgres
データベース一覧
-[ RECORD 1 ]--------+----------------
名前                 | postgres
所有者               | masahiro.tomita
エンコーディング     | UTF8
ロケールプロバイダー | libc
照合順序             | ja_JP.UTF-8
Ctype(変換演算子)    | ja_JP.UTF-8
ICUロケール          | 
ICUルール:           | 
アクセス権限         | 

この状態で ORDER BY すると OS と同じアホな子になってることがわかる。

postgres=# create table test (c varchar);
CREATE TABLE
postgres=# insert into test (c) values ('あああああ'),('いいいい'),('ううう'),('ええ'),('');
INSERT 0 5
postgres=# select * from test;
     c      
------------
 あああああ
 いいいい
 ううう
 ええ
 お
(5 行)

postgres=# select * from test order by c;
     c      
------------
 お
 ええ
 ううう
 いいいい
 あああああ
(5 行)

RDB の Collation による妙な挙動で MySQL の寿司ビール問題を思い出したわ。

sort でやったのと同じように initidb 時に --lc-collate=C を指定したらうまくいった。

$ initdb -U postgres --lc-collate=C /tmp/hoge
postgres=# \l postgres
データベース一覧
-[ RECORD 1 ]--------+------------
名前                 | postgres
所有者               | postgres
エンコーディング     | UTF8
ロケールプロバイダー | libc
照合順序             | C
Ctype(変換演算子)    | ja_JP.UTF-8
ICUロケール          | 
ICUルール:           | 
アクセス権限         | 


postgres=# select * from test order by c;
     c      
------------
 あああああ
 いいいい
 ううう
 ええ
 お
(5 行)

しかし、エンコーディングがUTF-8 なのに COLLATE が C というのはどうなんだろうなー…と思って、PostgreSQL は libc ロケール以外にも ICU ロケールが使えるので、それを指定することにした。

$ initdb -U postgres --locale-provider=icu --icu-locale=ja_JP.UTF-8 /tmp/hoge
postgres=# \l postgres
データベース一覧
-[ RECORD 1 ]--------+------------
名前                 | postgres
所有者               | postgres
エンコーディング     | UTF8
ロケールプロバイダー | icu
照合順序             | ja_JP.UTF-8
Ctype(変換演算子)    | ja_JP.UTF-8
ICUロケール          | ja-JP
ICUルール:           | 
アクセス権限         | 

postgres=# select * from test order by c;
     c      
------------
 あああああ
 いいいい
 ううう
 ええ
 お
(5 行)

しかし macOS は UI が使いにくいとは思っていたが OS としてもダメだったとはなぁ。 いやまあ macOS なんて使わずに Docker コンテナ使えばまともな Linux が使えるんだけど、あまりにも遅いんで…。

ていうか、開発にデスクトップ Linux 使いたい。