プログラミングで範囲を扱う際、下記のinclusiveとexclusive、どちらが良いか?
inclusive [a, b] a <= x <= b
exclusive [a, b) a <= x < b
以前になぜ区間は[closed, open)での半開区間で表現すべきか?という記事でダイクストラの意見を紹介したが、最近どうもinclusiveの方が良いのではないかという気がしており、ちょっと再考してみた。
本記事では以下の観点から考察する。
- 一般的に使われるのはどちらか
- 長さの計算
- 重なりの無い範囲に分割できるか
- 空の範囲の表現
- 全体の表現
考えるデータ型は整数、浮動小数点数、PostgreSQLのtimestamp型(精度マイクロ秒)とする。
※PostgreSQLのtimestamp型は、内部では2000-01-01 00:00からの経過マイクロ秒を64ビット整数で保持している。デフォルトでは小数点以下6桁(マイクロ秒)の精度だが、timestamp(N)
のように定義することでN桁の精度にできる。例えばN=0にすると秒単位となり、2000-01-01 00:00:00.123456
を登録しようとすると2000-01-01 00:00:00
に丸められる。
1. 一般的に使われるのはどちらか
日常生活で使われる範囲はinclusiveが多い。ゲームなどで「10〜20ポイント」といったら20を含むし、「1月1日〜1月31日」と書いたら31日を含むし、「0:00〜23:59」のように書いたら23:59は23:59:00ちょうどではなく分の精度で考えて23:59を含むという意味であり、23:59:59もこの範囲に含まれると解釈されるだろう。
しかし一方、勤務時間や開店時間などで「9時〜18時」のような表記も見かける。これは18時台を含まない事が多い。18時正時(しょうじ)までなのだろう。
「9:00〜18:00」だったらどうだろうか?18:00:59まで勤務/開店しなければならないのだろうか?これも18時正時を表しているように思われる。inclusive/exclusiveはどちらでもいいのだろう。
契約書などでよく見られる「自○年至○年…」という表現も、inclusiveと解釈されるのが一般的である。
ChatGPTによると、航空券やホテル予約など一部の商慣習では「〜まで」は 当日を含まない解釈もあるとのこと。
数学では、実数の範囲はexclusiveで表現すると高校で教わった人も多いだろう。
これは、[a, b] [b, c]というようにinclusiveだと、bが両方の範囲に含まれてしまって都合が悪いからである。[a, b) [b, c)にすれば重なりのない範囲に分割できる。
有理数の場合も同様である。
プログラミングにおいて、配列のスライスはexclusiveであることが多い。JavaScript、 Python、 Go
UnityのRandom.Rangeは、なんとintはexclusiveだが、floatはinclusiveである。(何か理由があるのだろうか?分かる人がいたら教えて下さい)
public static int Range (int minInclusive, int maxExclusive);
public static float Range (float minInclusive, float maxInclusive);
SQLのBETWEENはinclusiveである。
bash/zshのfor i in {0..3}
はinclusiveである。
2. 長さの計算
整数の場合
[a, b)の長さはb - aだが、
[a, b]の長さはb - a + 1となる。exclusiveの方が少しだけシンプルである。配列のスライスがexclusiveなのはこれが主な理由だと思われる。
timestamp型の場合
[2000-01-01 00:00:00.000000, 2000-01-02 00:00:00.000000)のマイクロ秒数は86400000000
[2000-01-01 00:00:00.000000, 2000-01-01 23:59:59.999999]のマイクロ秒数は86400000000
inclusiveの場合はやはり+1が必要になる。
3. 重なりの無い範囲に分割できるか
前述のように、数学で実数の範囲を分割する際はexclusiveが一般的である。
では、整数、浮動小数点数、timestamp型の場合はどんな問題があるのだろうか?
整数の場合
[0, 9]を真ん中で分割すると [0, 4] [5, 9] になる。
[0, 10)を真ん中で分割すると [0, 5) [5, 10) になる。
どちらも特に問題は見当たらない。
浮動小数点数の場合
exclusiveの場合は特に問題がない。
inclusiveだと、数学の実数と同様の問題がある。
[0.0f, 1.0f] を[0.0f, 0.5f] [0.5f, 1.0f] に分割すると0.5fが両方に含まれてしまう。
実際には浮動小数点数は有限精度だから0.5fの「隣の数」が存在し、[0.0f, 0.5f] [0.5fの右隣の数, 1.0f]と分割すれば重なりは無くなるが、隣の数を求めるのが難しく、現実的ではない(ChatGPTに聞くとアルゴリズムを出してくれた)。
timestamp型の場合
timestamp型の範囲は、以下のように、指定期間に含まれる行を取得する際によく現れる:
SELECT * FROM items WHERE '2000-01-01 00:00:00' <= createad_at AND created_at < '2000-01-01 01:00:00';
[0:00:00, 1:00:00] [1:00:00, 2:00:00]という分割だと、1:00:00の行が両方に含まれ、二重取得になってしまう。
[0:00:00, 0:59:59] [1:00:00, 1:59:59]という分割だと、0:59:59.000001のような値を取りこぼしてしまう。もっとも、データ登録時に必ず秒単位に丸めてから登録するようにすれば、問題はない。
[0:00:00, 0:59:59.999999] [1:00:00, 1:59:59.999999]という分割にすれば、二重取得も取りこぼしもない。
重なりがあっても問題ないか?
[0:00:00, 1:00:00] [1:00:00, 2:00:00]という分割で二重取得になっても、アプリケーション側の処理で片方から1:00:00の行を除外できるなら、一応問題は解消する。
3. 空の範囲の表現
inclusiveの場合、a > bなるa, bを用いて、[a, b]とすれば良い。例:[1, 0]
exclusiveの場合、任意のaを用いて[a, a)で良い。
4. 全体の表現
符号なし8ビット整数の範囲全体を表したいとする。
inclusiveなら[0, 255]で特に問題ない。
exclusiveなら[0, 256)となる。256は8ビット整数に収まらなくなってしまう。[0, 0)だけは全体を表す、とでも約束しておけば8ビット整数で表現できるが、そういった特殊ルールが必要。
まとめ
観点 | inclusive | exclusive |
---|---|---|
日常で一般的 | ◯ | × |
プログラミングで一般的 | ◯ | ◯ |
長さの計算 | △ | ◯ |
整数範囲の分割 | ◯ | ◯ |
実数範囲の分割 | × | ◯ |
空範囲の表現 | ◯ | ◯ |
全体の表現 | ◯ | △ |
浮動小数点数で重なりの無い範囲に分割する必要がある際だけはexclusiveにする必要がある。
しかし、それ以外の場合は長短あり、甲乙つけがたい。
timestamp型は内部では整数であるし、範囲に重なりがあってもなんらか工夫で解決できないか考えてみるとよさそうだ。
いずれにせよ、範囲を扱うときはinclusiveなのかexclusiveなのか、必ずドキュメント化すべきだろう。変数名なども、単に max
でなく maxInclusive
や maxExlusive
のように明示することを検討されたい。
[a, b]という記法は配列と紛らわしいが、Unityでは
[minInclusive..maxInclusive]
[minInclusive..maxExclusive)
のように、カンマの代わりに ..
を用いているようだ。良いと思う。
日時は精度を明示するとより厳密になる。
例えば [09:00..23:59](秒は切り捨て)
と書けば23:59:59
がこの範囲に含まれるかは明らかになる。
コメント歓迎。