<8> 集合型 (標準 Pascal 範囲内での Delphi 入門)


# 8. 集合型 (Sets)
集合は、同一の[順序型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#21-%E9%A0%86%E5%BA%8F%E5%9E%8B-%E9%A0%86%E5%BA%8F%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B)の値の集まりです。値に順序はありません。また、同じ値を格納する事にも意味はありません。集合型は空集合を表す事もできます。集合は抽象化されたビット配列です。

```
集合型 = 
  [packed] set of 基底型.
基底型 =
  順序型.
```

但し Pascal は通常、集合型に指定できる[順序型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#21-%E9%A0%86%E5%BA%8F%E5%9E%8B-%E9%A0%86%E5%BA%8F%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B)の範囲を制限していて、非常に小さいものしか定義できない場合もあります。

```pascal
type
  TSomeInts = 1..250;
  TIntSet1 = set of TSomeInts; (* 部分範囲型の集合                        *)
  TIntSet2 = set of 1..250;    (* こう書いても同じ                        *)
  TIntSet3 = set of 0..255;    (* 256 個の基底型                          *)
  TIntSet4 = set of 0..256;    (* NG: 基底型の要素数が 257 個ある         *)
  TIntSet5 = set of 200..300;  (* NG: 基底型の要素の値が 256 を超えている *)
```

Delphi の場合、**基底型**の要素数が 256 を超えてはいけませんし、その要素が取り得る値も 0～255 の範囲内である必要があります。

集合型の定義と同時に変数宣言する事が可能です。

```pascal
var MySet: set of 1..10;
```

例えば[文字型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#24-char-%E5%9E%8B)の集合であれば、

```pascal
var MyCharSet: set of Char;
```

とする事ができます。但し、[Char 型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#24-char-%E5%9E%8B) が常に 8 ビット (0～255) であるとは限りませんので、この書き方には注意が必要です。

**See also:**

 - [集合 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B%EF%BC%88Delphi%EF%BC%89#.E9.9B.86.E5.90.88)

## 8.1. 集合構成子 (Set Constructors)

集合値は要素の記述をカンマ `,` で区切るか、low`..`high 形式の範囲を指定し、全体を `[` `]` で括ったものです。これを**集合構成子**といいます。

```
集合構成子 = 
  "[" [集合要素 {"," 集合要素} "]".
集合要素 = 
  式 [".." 式].
```

以下は集合構成子の例です。

```pascal
[1, 5, 10, 50, 100];
[i + 1, i + 2];
['A'..'Z'];
['0'..'9', '#', '*'];
```

集合構成子は他の集合の型との適合性を持つようにするため、パックの状態を持ちません (不定)。

**See also:**

 - [集合構成子 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%BC%8F%EF%BC%88Delphi%EF%BC%89#.E9.9B.86.E5.90.88.E6.A7.8B.E6.88.90.E5.AD.90)

## 8.2. 集合演算子 (Set Operations)

### (8.2.1.) 集合の代入
X を集合型変数、E を集合式とすると、

 1. E の要素がすべて X の基底型に含まれる。
 2. X と E が共に **set of** あるいは X と E が共に **packed set of**。

```pascal
X := E;
```

上記条件を満たした場合に代入が可能です。

Delphi では次の組み込みルーチンも使えます。

| 手続き |  説明 |
|:---:|:---|
| [Include(S, I)](http://docwiki.embarcadero.com/Libraries/ja/System.Include) |S := S + [I]; と同等 |
| [Exclude(S, I)](http://docwiki.embarcadero.com/Libraries/ja/System.Exclude) |S := S - [I]; と同等 |

**See also:**

 - [集合演算子 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%BC%8F%EF%BC%88Delphi%EF%BC%89#.E9.9B.86.E5.90.88.E6.BC.94.E7.AE.97.E5.AD.90)


### (8.2.2.) 集合の演算
A と B が同じ集合型の場合に次の集合演算が可能です。

| 演算 | 集合 |数学記号| 説明 |
|:---:|:---|:---:|:---|
| A + B |和集合|A ∪ B |A と B のすべての要素の集合|
| A - B |差集合|A ＼ B|A にあって B にない要素の集合|
| A * B |積集合|A ∩ B|A と B に共通の集合|

```pascal:Setstest.pas
program SetsTest1(output);
type
  TDayOfTheWeek = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);  
var
  A, B, C: set of TDayOfTheWeek;
  Day: TDayOfTheWeek;
begin
  A := [Wed..Fri];
  B := [Mon, Wed, Fri, Sun];

  C := A + B;
  for Day:=Mon to Sun do
    begin
      if Day in C then
        begin
          Write(Ord(Day));
          Write(' ');
        end;  
    end;
  Writeln;
  
  C := A - B;
  for Day:=Mon to Sun do
    begin
      if Day in C then
        begin
          Write(Ord(Day));
          Write(' ');
        end;  
    end;
  Writeln;

  C := A * B;
  for Day:=Mon to Sun do
    begin
      if Day in C then
        begin
          Write(Ord(Day));
          Write(' ');
        end;  
    end;
  Writeln;
end.
```

上記プログラムの実行結果は次のようになります。

```
    0   2   3   4   6
    3
    2   4
```

### (8.2.3.) 集合の比較演算

次の比較演算も行えます。

| 演算 | 意味 | 数学記号 | 説明 |
|:---:|:---:|:---:|:---|
| e **in** A |帰属関係| e ∈ A | e が A の要素であれば **True**|
| A = B |等しい| A ＝ B | A と B が同じ集合であれば **True**|
| A \<\> B |等しくない| A ≠ B | A と B が異なる集合であれば **True**|
| A \<= B |サブセット| A ⊂ B| A が B の部分集合であれば **True**|
| A \>= B |スーパーセット| A ⊃ B | B が A の部分集合であれば **True**|

※ **in** 演算子は[順序型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#21-%E9%A0%86%E5%BA%8F%E5%9E%8B-%E9%A0%86%E5%BA%8F%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B)に対しても使えます。

```pascal:SetsTest2.pas
program SetsTest2(output);
type
  TDayOfTheWeek = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);  
var
  A, B, C: set of TDayOfTheWeek;
  BoolStr: array [Boolean] of packed array [1..5] of Char;
  v: Boolean;
begin
  BoolStr[True ] := 'True ';
  BoolStr[False] := 'False';

  A := [Wed..Fri];
  B := [Mon, Wed, Fri, Sun];
  C := [Mon..Sun] - B; (* [Tue, Thu, Sat] *)
  
  { e in A }
  v := Mon in A;
  Writeln(BoolStr[v]);
  
  { A = B }
  v := (A = B);
  Writeln(BoolStr[v]);

  { A <> B }
  v := (A <> B);
  Writeln(BoolStr[v]);

  { A <= B }
  v := (A <= B);
  Writeln(BoolStr[v]);
  
  { A >= B }
  v := (A >= B);
  Writeln(BoolStr[v]);

  { A <= B + C }
  v := (A <= B + C);
  Writeln(BoolStr[v]);

  { B + C >= A }
  v := (B + C >= A);
  Writeln(BoolStr[v]);
end.
```

上記プログラムの実行結果は次のようになります。

```
False
False
True
False
False
True
True
```

次のコードは、数値を 3 度入力して条件に合致したら `Hello!World.` と表示するコンソールアプリケーションです。

```pascal:SetsTest3.pas
program SetsTest3(input, output);
var
  a, b, c: Integer;
begin
  repeat
    Writeln('*');
    Readln(a);
    Readln(b);
    Readln(c);

    if (a = 2) and (b = 2) and (c = 2) then { (A) }
      Write('Hello!');

    if [a, b, c] = [2] then                 { (B) }
      Writeln('World.');

  until ([a, b, c] = [0]); { すべて 0 を入力すると抜ける }
end.
```

(A) と (B) の条件式は等価なので、`Hello!` だけ表示されたり `World.` だけ表示されたりする事はありません。

```
*
1
2
3
*
1
1
1
*
2
2
2
Hello!World.
*
3
3
3
*
```

同様に、次のような比較演算が可能です。

| (A) | (B) | 条件 |
|:---|:---|:---|
| **if** (a = 2) **and** (b = 2) **and** (c = 2) **then** | **if** [a, b, c] = [2] **then** | a,b,c すべてが 2 |
| **if** (a \<\> 2) **and**  (b \<\> 2) **and** (c \<\> 2) **then** | **if** **not** (2 **in** [a, b, c]) **then**<br>または<br>**if** **not**([a, b, c] >= [2]) **then**  | a,b,c すべてが 2 ではない|
| **if** (a = 2) **or** (b = 2) **or** (c = 2) **then** | **if** (2 **in** [a, b, c]) **then**<br>または<br>**if** [a, b, c] \>= [2] **then** | a,b,c どれかが 2 |
| **if** (a \<\> 2) **or** (b \<\> 2) **or** (c \<\> 2) **then** | **if** [a, b, c] \<\> [2] **then** | a,b,c どれかが 2 ではない|

集合を使った比較演算は**完全論理評価**になるため、a,b,c が関数だった場合にはすべて評価されてしまいます。副作用に気を付けましょう。

**See also:**

 - [集合演算子 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%BC%8F%EF%BC%88Delphi%EF%BC%89#.E9.9B.86.E5.90.88.E6.BC.94.E7.AE.97.E5.AD.90)

### (8.2.4.) ビット集合 (Bitset)
集合は抽象化されたビット配列です。Delphi だと同じサイズの型へキャスト ([10.7.2 節](https://qiita.com/ht_deko/items/745ad4a792d277d33831#1072-%E5%A4%89%E6%95%B0-%E5%9E%8B%E3%82%AD%E3%83%A3%E3%82%B9%E3%83%88)) できます。

```pascal:BitTest.pas
program BitTest;
{$APPTYPE CONSOLE}

type
  TBits8 = packed set of 0..7;

var
  P, Q, V: TBits8;
  B: Byte;
begin
  P := [7, 6, 4, 2, 0];     (* 11010101 *)
  Q := [7, 6, 5, 3, 1];     (* 11101010 *)

  (* NOT P *)
  V := [0..7] - P;          (* 00101010 *)
  Writeln(Byte(V));         (* 42 *)

  (* P OR Q *)
  V := P + Q;               (* 11111111 *)
  Writeln(Byte(V));         (* 255 *)

  (* P AND Q  *)
  V := P * Q;               (* 11000000 *)
  Writeln(Byte(V));         (* 192 *)

  (* P XOR Q *)
  V := (P * ([0..7] - Q)) +
       (([0..7] - P) * Q);  (* 00111111 *)
  Writeln(Byte(V));         (* 63 *)
end.
```

[列挙型](https://qiita.com/ht_deko/items/335e59857cc78aea951a#51-%E5%88%97%E6%8C%99%E5%9E%8B-enumerated-types) や **絶対変数 (absolute)** と組み合わせる事もできます。

```pascal:BitTest2.dpr
program BitTest2;
{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  TBits8 = (b0, b1, b2, b3, b4, b5, b6, b7);
var
  Bits8: set of TBits8;
  ByteData: byte absolute Bits8;
begin
  Bits8 := [b7, b0];                    // 10000001
  Writeln('0x', IntToHex(ByteData, 2)); // 0x81
  Include(Bits8, b3);                   // 10001001
  Bits8 := Bits8 + [b4];                // 10011001
  Writeln('0x', IntToHex(ByteData, 2)); // 0x99 
end.
```

**See also:**

 - [(10.7.2.) 変数 型キャスト (Qiita)](https://qiita.com/ht_deko/items/745ad4a792d277d33831#1072-%E5%A4%89%E6%95%B0-%E5%9E%8B%E3%82%AD%E3%83%A3%E3%82%B9%E3%83%88)
 - [変数型キャスト (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%BC%8F%EF%BC%88Delphi%EF%BC%89#.E5.A4.89.E6.95.B0.E5.9E.8B.E3.82.AD.E3.83.A3.E3.82.B9.E3.83.88)
 - [絶対アドレス (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E5%A4%89%E6%95%B0%EF%BC%88Delphi%EF%BC%89#.E7.B5.B6.E5.AF.BE.E3.82.A2.E3.83.89.E3.83.AC.E3.82.B9)
 - [ビットフィールドを操作する (ht-deko.com)](https://ht-deko.com/tech076.html)

### (8.2.5.) 集合定数とグローバル集合変数の初期化

Delphi では次のような集合定数が使えます。

```pascal:SetsConstantsTest.pas
program ArrayConstantsTest;
{$APPTYPE CONSOLE}

// グローバル集合定数
const
  MyCharSet1: set of AnsiChar = ['A', 'B'..'F'];

// グローバル集合変数と初期化
var
  MyCharSet2: set of AnsiChar = ['A', 'B'..'F'];

  procedure Sub;
//var
//  ローカル集合変数は初期化できない
//  MyCharSet3: set of AnsiChar = ['A', 'B'..'F'];
  begin
    ...
  end; { Sub }
begin
  ...
end.
```

## 8.3. プログラムの開発について (On Program Development)
章構成を参考にした ["PASCAL 原書第 4 版 (ISBN: 4563014664)"](http://book.tsuhankensaku.com/hon/isbn/9784563014667/) のこの節では、集合を用いた**エラトステネスのふるい**が 3 つ紹介されています。

| Program | 動作 | 説明 |
|:---:|:---:|:---|
| Prime1 |×|エラトステネスのふるい|
| Prime2 |×|奇数だけの集合を使った<br>エラトステネスのふるい|
| Prime3 |〇|奇数だけの集合を使った<br>エラトステネスのふるい|

Prime1 と Prime2 が動作しないのは、

```pascal
const
  N = 10000;
type
  Positive = 1..MaxInt;
var
  Sieve, Primes: set of 2..N;
  NextPrime, Multiple: Positive;
```

このような、**10,000 個の要素が使える集合型を持つ Pascal** という有り得ない想定の下で書かれた**疑似コード**だからです...ひょっとしたら世の中にはそんな Pascal があるのかもしれませんけれど。

せっかくなので、ちゃんと動作する Prime3 のコードを転載しておきます。

```pascal:Prime3.pas
program Prime3(Output);
{ プログラム 8.5 - 3..10000の範囲内で、奇数だけを含むふるい
                  を使って素数を求める。 }
const
  SetSize = 128 { 処理系依存; >= 2 };
  MaxElement = 127 { SetSize - 1 };
  SetParts = 39 { = 10000 div Setsize div 2 };

type
  Natural = 0..MaxInt;

var
  Sieve, Primes:
    array [0..SetParts] of
      set of 0..MaxElement;
  NextPrime:
    record
      Part: 0..SetParts;
      Element: 0..MaxElement
    end;
  Multiple, NewPrime: Natural;
  P, N, Count: Natural;
  Empty: Boolean;

begin { 初期設定 }
  for P := 0 to SetParts do
    begin
      Sieve[P] := [0..MaxElement];
      Primes[P] := []
    end;
  Sieve[0] := Sieve[0] - [0];
  Empty := False;
  NextPrime.Part := 0;
  NextPrime.Element := 1;
  with NextPrime do
    repeat { 次の素数を求める }
      while not(Element in Sieve[Part]) do
        Element := Succ(Element);
      Primes[Part] := Primes[Part] + [Element];
      NewPrime := 2 * Element + 1;
      Multiple := Element;
      P := Part;
      while P <= SetParts do { 除去 }
        begin
          Sieve[P] := Sieve[P] - [Multiple];
          P := P + Part * 2;
          Multiple := Multiple + NewPrime;
          while Multiple > MaxElement do
            begin
              P := P + 1;
              Multiple := Multiple + SetSize;
            end
        end;
      if Sieve[Part] = [] then
        begin
          Empty := True;
          Element := 0
        end;
      while Empty and (Part < SetParts) do
        begin
          Part := Part + 1;
          Empty := Sieve[Part] = []
        end
      until Empty;
      Count := 0;
      for P := 0 to SetParts do
        for N := 0 to MaxElement do
          if N in Primes[P] then
            begin
              Write(Output, 2 * N + 1 + P * SetSize * 2:6);
              Count := Count + 1;
              if (Count mod 8) = 0 then
                Writeln(Output)
            end
end.
```

このコードは割と真面目な "エラトステネスのふるい" なので、0～10000 (正確には 0～9983) から素数を見つけるには相当時間が掛かります。ひょっとするとメモリ不足で実行できないかもしれません。

```pascal
const
  SetSize = 256;
  MaxElement = 255;
  SetParts = 0;
```

このような設定にすると (環境依存ですが) 0～511 の範囲の素数を探します。

**See also:**

 - [エラトステネスの篩 (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%A9%E3%83%88%E3%82%B9%E3%83%86%E3%83%8D%E3%82%B9%E3%81%AE%E7%AF%A9)

### (8.3.1. ) Unicode 版 Delphi と CharInSet() 関数

[Char 型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#24-char-%E5%9E%8B)のサイズは環境依存です。標準 Pascal では [Char 型](https://qiita.com/ht_deko/items/5c04bb2b42947e29d94b#24-char-%E5%9E%8B)が 8 ビット以下である前提のサンプルコードがあり、この集合型は最たるものです。

バージョン 2009 以降、Delphi は 文字列型 / 文字型を Unicode に移行しました。内部で使われている符号化形式は Windows に合わせて UTF-16LE となっています。つまり、文字の 1 要素のサイズが 16 ビットになりました。これに伴い、Char 型も 16 ビットの WideChar と同じになりました。

そうなると困るのが集合型と共によく使われる **in** 演算子です。集合型の基底型は

 - 要素数は 256 個以内
 - 要素の値は 255 を超えてはいけない

という事でした。例えば次のようなコードがあったとします。

```pascal
var
  MyCharSet: set of Char;
  c: Char;
begin
  MyCharSet := ['0'..'9', '*', '#'];
  c := '7';
  if c in MyCharSet then
    Writeln('OK!');
end.
```

**ANSI 版 Delphi** の Char 型は AnsiChar (8 ビット) なので何の問題もありません。

```pascal
  MyCharSet: set of Char;
```

しかしながら **Unicode 版 Delphi** の Char 型は UnicodeChar (16 ビット) なので集合型の基底値には使えません。正確に言うと使えるには使えるのですが、範囲が 0x0000～0x00FF までに制限されます。

**Unicode 版 Delphi** で 8 ビットの Char (AnsiChar) を使ったコードは次の通りです。

```pascal
var
  MyCharSet: set of AnsiChar;
  c: AnsiChar;
begin
  MyCharSet := ['0'..'9', '*', '#'];
  c := '7';
  if c in MyCharSet then
    Writeln('OK!');
end.
```

このコードには問題がある可能性があります。このロジックが**文字を判定するもの**だったとしたらどうでしょうか？文字列が ANSI から Unocode に変わっても**文字を判断しなければならない**としたら？

それでも多くの場合、処理している文字が ASCII 7 ビットの範囲内であれば、処理は正しく行われます。W1050 のワーニングが出ますが、これは `CharInSet()` 関数を使う事で回避できます。

例えば次のようなコードは

```pascal
program CharInSetTest;
{$APPTYPE Console}
var
  c: Char;
begin
  c := '7';
  if c in ['0'..'9', '*', '#'] then
    Writeln('OK!');
end.
```

このように変更できます。

```pascal
program CharInSetTest;
{$APPTYPE Console}

uses
  System.SysUtils;

var
  c: Char;
begin
  c := '7';
  if CharInSet(c, ['0'..'9', '*', '#']) then
    Writeln('OK!');
end.
```

CharInSet() は組み込みルーチンではないので、System.SysUtils を **uses** 句に追加する必要があります。

**See also:**

 - [System.SysUtils.CharInSet (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.CharInSet)
 - [Set of Char 構造 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/RAD_Studio_%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B_Unicode#Set_of_Char_.E6.A7.8B.E9.80.A0)
 - [W1050 設定式で WideChar がバイト文字に変換されました (Delphi)](http://docwiki.embarcadero.com/RADStudio/ja/W1050_%E8%A8%AD%E5%AE%9A%E5%BC%8F%E3%81%A7_WideChar_%E3%81%8C%E3%83%90%E3%82%A4%E3%83%88%E6%96%87%E5%AD%97%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%95%E3%82%8C%E3%81%BE%E3%81%97%E3%81%9F_(Delphi))

### (8.3.2. ) Pascal の字下げ (インデント) スタイル
この節は完全に余談です。

**J&W (第 4 版)** のサンプルコードのインデントスタイルは一貫しており、C 言語で言うところの **GNU スタイル**で書かれています。**J&W スタイル**とでも名付けましょうか。

```pascal
 while (x = y) do
   begin
     something;
     somethingelse;
   end;
 finalthing;
```

これに対し、Borland 系の Pascal (Turbo Pascal, Borland Pascal, Delphi) は **Borland スタイル**で書かれています。これは C 言語で言うところの **BSD/オールマンのスタイル**です。

```pascal
 while (x = y) do
 begin
   something;
   somethingelse;
 end;
 finalthing;
```

Wikipedia の **BSD/オールマンのスタイル** の項には

> これはPascalやTransact-SQLの標準的な字下げに似ていて、

と書かれていますが、Pascal としては **J&W スタイル** と **Borland スタイル**のどちらが標準的なのかは意見が分かれる所だろうと思います。ただ、現在の Pascal コードで圧倒的に目にする事が多いのは **Borland スタイル**です。Delphi のソースコードを読む上でも **Borland スタイル**に慣れておいたほうがいいと思います。

Delphi でソースコードの整形は〔Ctrl〕+〔D〕またはコードエディタのコンテキストメニューから行えます。デフォルトは当然 **Borland スタイル**です。

ソースコードを **J&W スタイル**で整形したい場合には [ツール | オプション] の [フォーマッタ | Delphi | インデント] で **begin および end キーワードのインデント** を "はい" に設定します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/38082328-f5eb-6174-7fcd-3b22471143d4.png)
C# のコーディング規則が **BSD/オールマンのスタイル**に則っているのは...[察してください](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%B3%E3%83%80%E3%83%BC%E3%82%B9%E3%83%BB%E3%83%98%E3%83%AB%E3%82%B9%E3%83%90%E3%83%BC%E3%82%B0)。

**See also:**

 - [GNU スタイル - 字下げ (Wikipedia)](https://ja.wikipedia.org/wiki/%E5%AD%97%E4%B8%8B%E3%81%92%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB#GNU%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB)
 - [BSD/オールマンのスタイル - 字下げ (Wikipedia)](https://ja.wikipedia.org/wiki/%E5%AD%97%E4%B8%8B%E3%81%92%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB#BSD/%E3%82%AA%E3%83%BC%E3%83%AB%E3%83%9E%E3%83%B3%E3%81%AE%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB)


# 索引
[ [← 7. レコード型](https://qiita.com/ht_deko/items/1d2c85768febc95009bf) ] [ [↑ 目次へ](https://qiita.com/ht_deko/items/e2796788c65c2cda1de5) ] [ [→ 9. ファイル型](https://qiita.com/ht_deko/items/986fa01c9e6cfc3fd673) ] [:sushi:](https://qiita.com/ht_deko/items/a0044d6decd6f175365f)