🤪

macOSのソートがアホで困る

2024/12/02に公開

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

https://qiita.com/advent-calendar/2024/smarthr

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

https://x.com/tmtms/status/1859033067115213118

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

https://blog.zhimingwang.org/macos-lc_collate-hunt

https://ja.stackoverflow.com/questions/100019/

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 の寿司ビール問題を思い出したわ。

https://twitter.com/tmtms/status/546925668424896512

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 使いたい。

Discussion