LoginSignup
2
4

More than 5 years have passed since last update.

PHPの連想配列で複数カラムを指定してGROUP BYしたい。

Posted at

こんな配列をGroup Byしたい。

GroupByArray.php
/** @var array 温泉地テーブル. */
private const TABLE = [
    ['kuni' => '日本', 'todofuken' => '群馬県', 'shi' => '吾妻郡', 'onsen' => '草津'],
    ['kuni' => '日本', 'todofuken' => '群馬県', 'shi' => '渋川市', 'onsen' => '伊香保'],
    ['kuni' => '日本', 'todofuken' => '神奈川県', 'shi' => '足柄下郡', 'onsen' => '湯河原'],
    ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '観海寺'],
    ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '浜脇'],
    ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '堀田'],
    ['kuni' => 'ドイツ', 'todofuken' => 'ヴュルテンベルク州', 'shi' => 'バーデン=バーデン', 'onsen' => 'フリードリヒス'],
    ['kuni' => 'ドイツ', 'todofuken' => 'ヴュルテンベルク州', 'shi' => 'バーデン=バーデン', 'onsen' => 'カラカラ'],
    ['kuni' => 'ドイツ', 'todofuken' => 'ヘッセン州', 'shi' => 'ヴィースバーデン', 'onsen' => 'アウカムタル'],
    ['kuni' => 'ドイツ', 'todofuken' => 'ヘッセン州', 'shi' => 'ヴィースバーデン', 'onsen' => 'カイザーフリードリヒ'],
];

foreachgroup by

キーでグループ化する - Qiita を参考にさせていただきました。

GroupByArray.php
// 省略
/**
 * 2つの項目でGROUP BYする.
 * @param  string $key1 1番目のグループ化する項目の名前.
 * @param  string $key2 2番目のグループ化する項目の名前.
 */
public function groupByTwoKey(string $key1, string $key2): void
{
    $grouped = [];
    foreach (self::TABLE as $row) {
        $key = $row[$key1].$row[$key2];
        $grouped[$key][] = $row;
    }
    return $grouped;
}
// 省略
$groupBy = new GroupByArray();
var_dump($groupBy->groupByTwoKey('kuni','todofuken'));
キーにしたカラムが残ってちょっとうざい
$ php GroupByArray.php 
array(5) {
  ["日本群馬県"]=>
  array(2) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(9) "群馬県"
      ["shi"]=>
      string(9) "吾妻郡"
      ["onsen"]=>
      string(6) "草津"
    }
    [1]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(9) "群馬県"
      ["shi"]=>
      string(9) "渋川市"
      ["onsen"]=>
      string(9) "伊香保"
    }
  }
  ["日本神奈川県"]=>
  array(1) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(12) "神奈川県"
      ["shi"]=>
      string(12) "足柄下郡"
      ["onsen"]=>
      string(9) "湯河原"
    }
  }
  ["日本大分県"]=>
  array(3) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(9) "大分県"
      ["shi"]=>
      string(9) "別府市"
      ["onsen"]=>
      string(9) "観海寺"
    }
    [1]=>
    array(4) {
      ["kuni"]=>
# 省略

group byのキーにしたカラムはいらない場合

GroupByArray->groupByTwoKey
// 省略
    foreach (self::TABLE as $row) {
        /** @var array $notKeys キーにする要素を削除した配列. */
        $notKeys = array_filter($row, function($key) use ($key1, $key2) {
            return $key !== $key1 && $key !== $key2;
        }, ARRAY_FILTER_USE_KEY);

        $key = $row[$key1].$row[$key2];
        $grouped[$key][] = $notKeys;
// 省略
$ php GroupByArray.php 
array(5) {
  ["日本群馬県"]=>
  array(2) {
    [0]=>
    array(2) {
      ["shi"]=>
      string(9) "吾妻郡"
      ["onsen"]=>
      string(6) "草津"
    }
    [1]=>
    array(2) {
      ["shi"]=>
      string(9) "渋川市"
      ["onsen"]=>
      string(9) "伊香保"
    }
  }
  ["日本神奈川県"]=>
  array(1) {
    [0]=>
    array(2) {
      ["shi"]=>
      string(12) "足柄下郡"
      ["onsen"]=>
      string(9) "湯河原"
    }
  }
  ["日本大分県"]=>
  array(3) {
    [0]=>
    array(2) {
      ["shi"]=>
      string(9) "別府市"
      ["onsen"]=>
      string(9) "観海寺"
    }
    [1]=>
    array(2) {
      ["shi"]=>
      string(9) "別府市"
      ["onsen"]=>
# 省略

group byのキーにしたカラム毎に階層を作りたい場合

GroupByArray->groupByTwoKey
// 省略
        /** @var array $notKeys キーにする要素を削除した配列. */
        $notKeys = array_filter($row, function($key) use ($key1, $key2) {
            return $key !== $key1 && $key !== $key2;
        }, ARRAY_FILTER_USE_KEY);

        $grouped[$row[$key1]][$row[$key2]][] = $notKeys;
// 省略
$ php GroupByArray.php 
array(2) {
  ["日本"]=>
  array(3) {
    ["群馬県"]=>
    array(2) {
      [0]=>
      array(2) {
        ["shi"]=>
        string(9) "吾妻郡"
        ["onsen"]=>
        string(6) "草津"
      }
      [1]=>
      array(2) {
        ["shi"]=>
        string(9) "渋川市"
        ["onsen"]=>
        string(9) "伊香保"
      }
    }
    ["神奈川県"]=>
    array(1) {
      [0]=>
      array(2) {
        ["shi"]=>
        string(12) "足柄下郡"
        ["onsen"]=>
        string(9) "湯河原"
# 省略

array_reducegroup by

集計するとき、array_reduceと無名関数が便利だった! - maeharinの日記を参考にさせていただきました。
使い方は、ドキュメントを見ました(array_reduce — コールバック関数を用いて配列を普通の値に変更することにより、配列を再帰的に減らす)

GroupByArray.php
/**
 * 3つの項目でGROUP BYする.
 * @param  string $key1 1番目のグループ化する項目の名前.
 * @param  string $key2 2番目のグループ化する項目の名前.
 * @param  string $key3 3番目のグループ化する項目の名前.
 */
public function groupByTreeKey(string $key1, string $key2, string $key3): void
{
    $grouped = array_reduce(self::TABLE, function($grouped, $row) use ($key1, $key2, $key3) {
        $key = $row[$key1].$row[$key2].$row[$key3];
        $grouped[$key][] = $row;
        return $grouped;
    });
    return $grouped;
}
// 省略
$groupBy = new GroupByArray();
var_dump($groupBy->groupByTreeKey('kuni','todofuken', 'shi'));
とてもうざい
$ php GroupByArray.php 
array(6) {
  ["日本群馬県吾妻郡"]=>
  array(1) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(9) "群馬県"
      ["shi"]=>
      string(9) "吾妻郡"
      ["onsen"]=>
      string(6) "草津"
    }
  }
  ["日本群馬県渋川市"]=>
  array(1) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(9) "群馬県"
      ["shi"]=>
      string(9) "渋川市"
      ["onsen"]=>
      string(9) "伊香保"
    }
  }
  ["日本神奈川県足柄下郡"]=>
  array(1) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
      ["todofuken"]=>
      string(12) "神奈川県"
      ["shi"]=>
      string(12) "足柄下郡"
      ["onsen"]=>
      string(9) "湯河原"
    }
  }
  ["日本大分県別府市"]=>
  array(3) {
    [0]=>
    array(4) {
      ["kuni"]=>
      string(6) "日本"
# 省略

group byのキーにしないカラムを連結したい場合

GroupByArray->groupByTreeKey
/** @var string カンマ. */
private const COMMA = ',';
// 省略
    $grouped = array_reduce(self::TABLE, function($grouped, $row) use ($key1, $key2, $key3) {
        $notKeys = array_filter($row, function($key) use($key1, $key2, $key3) {
            return $key !== $key1 && $key !== $key2 && $key !== $key3;
        }, ARRAY_FILTER_USE_KEY);
        $notKey = implode(self::COMMA, $notKeys);

        $key = $row[$key1].$row[$key2].$row[$key3];

        if (array_key_exists($key, $grouped)) {
            $grouped[$key] .= self::COMMA.$notKey;
        } else {
            $grouped[$key] = $notKey;
        }

        return $grouped;
    }, array());
// 省略
$ php GroupByArray.php 
array(6) {
  ["日本群馬県吾妻郡"]=>
  string(6) "草津"
  ["日本群馬県渋川市"]=>
  string(9) "伊香保"
  ["日本神奈川県足柄下郡"]=>
  string(9) "湯河原"
  ["日本大分県別府市"]=>
  string(23) "観海寺,浜脇,堀田"
  ["ドイツヴュルテンベルク州バーデン=バーデン"]=>
  string(34) "フリードリヒス,カラカラ"
  ["ドイツヘッセン州ヴィースバーデン"]=>
  string(49) "アウカムタル,カイザーフリードリヒ"
}

group byしてフォーマットに埋め込む

文字列リテラルに式展開 - Qiitaで思いつきました。
素晴らしく汎用性のないものとなりました。

GroupByArray
/**
 * group byした結果をフォーマットに埋め込む.
 * @return string フォーマットした文字列.
 */
public function groupByAndFormat(): string
{
    $key1 = 'kuni';
    $key2 = 'todofuken';
    $key3 = 'other';
    $format = "{$key1}{$key2}の温泉には、{$key3}があります。";

    /** @var array $groups group byした結果. */
    $groups = self::groupByTwoKey($key1, $key2);

    /** @var array $formated フォーマットした文字を格納する配列. */
    $formated = array();
    /** @var string $kuni kuniカラムの値. */
    foreach ($groups as $kuni => $todofukens) {
        /** @var string $todofuken todofukenカラムの値. */
        foreach ($todofukens as $todofuken => $others) {
            /** @var string $other group byのキーにしなかった要素を「と」でつないだ文字列. */
            $other = '';
            foreach ($others as $otherParts) {
                /** @var string $otherPart group byのキーにしなかった要素をくっつけた文字列. */
                $otherPart = implode($otherParts);
                if (strlen($other) !== 0) {
                    $other .= 'と';
                }
                $other .= $otherPart;
            }
            /** @var array 置換する文字のセット. */
            $replacePairs = [$key1 => $kuni, $key2 => $todofuken, $key3 => $other];
            $formated[] = strtr($format, $replacePairs);
        }
    }

    // 改行でつなげて1つの文字列にする.
    return implode(PHP_EOL, $formated);
}
// 省略
$groupBy = new GroupByArray();
var_dump($groupBy->groupByAndFormat());
$ php GroupByArray.php 
string(607) "日本の群馬県の温泉には、吾妻郡草津と渋川市伊香保があります。
日本の神奈川県の温泉には、足柄下郡湯河原があります。
日本の大分県の温泉には、別府市観海寺と別府市浜脇と別府市堀田があります。
ドイツのヴュルテンベルク州の温泉には、バーデン=バーデンフリードリヒスとバーデン=バーデンカラカラがあります。
ドイツのヘッセン州の温泉には、ヴィースバーデンアウカムタルとヴィースバーデンカイザーフリードリヒがあります。"

全貌

GroupByArra.php
<?php

class GroupByArray
{
    /** @var array 温泉地テーブル. */
    private const TABLE = [
        ['kuni' => '日本', 'todofuken' => '群馬県', 'shi' => '吾妻郡', 'onsen' => '草津'],
        ['kuni' => '日本', 'todofuken' => '群馬県', 'shi' => '渋川市', 'onsen' => '伊香保'],
        ['kuni' => '日本', 'todofuken' => '神奈川県', 'shi' => '足柄下郡', 'onsen' => '湯河原'],
        ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '観海寺'],
        ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '浜脇'],
        ['kuni' => '日本', 'todofuken' => '大分県', 'shi' => '別府市', 'onsen' => '堀田'],
        ['kuni' => 'ドイツ', 'todofuken' => 'ヴュルテンベルク州', 'shi' => 'バーデン=バーデン', 'onsen' => 'フリードリヒス'],
        ['kuni' => 'ドイツ', 'todofuken' => 'ヴュルテンベルク州', 'shi' => 'バーデン=バーデン', 'onsen' => 'カラカラ'],
        ['kuni' => 'ドイツ', 'todofuken' => 'ヘッセン州', 'shi' => 'ヴィースバーデン', 'onsen' => 'アウカムタル'],
        ['kuni' => 'ドイツ', 'todofuken' => 'ヘッセン州', 'shi' => 'ヴィースバーデン', 'onsen' => 'カイザーフリードリヒ'],
    ];

    /** @var string カンマ. */
    private const COMMA = ',';

    /**
     * 2つの項目でGROUP BYする.
     * @param  string $key1 1番目のグループ化する項目の名前.
     * @param  string $key2 2番目のグループ化する項目の名前.
     */
    public function groupByTwoKey(string $key1, string $key2): array
    {
        $grouped = [];
        foreach (self::TABLE as $row) {
            /** @var array $notKeys キーにする要素を削除した配列. */
            $notKeys = array_filter($row, function($key) use ($key1, $key2) {
                return $key !== $key1 && $key !== $key2;
            }, ARRAY_FILTER_USE_KEY);

            $grouped[$row[$key1]][$row[$key2]][] = $notKeys;
        }
        return $grouped;
    }

    /**
     * 3つの項目でGROUP BYする.
     * @param  string $key1 1番目のグループ化する項目の名前.
     * @param  string $key2 2番目のグループ化する項目の名前.
     * @param  string $key3 3番目のグループ化する項目の名前.
     */
    public function groupByTreeKey(string $key1, string $key2, string $key3): array
    {
        $grouped = array_reduce(self::TABLE, function($grouped, $row) use ($key1, $key2, $key3) {
            $notKeys = array_filter($row, function($key) use($key1, $key2, $key3) {
                return $key !== $key1 && $key !== $key2 && $key !== $key3;
            }, ARRAY_FILTER_USE_KEY);
            $notKey = implode(self::COMMA, $notKeys);

            $key = $row[$key1].$row[$key2].$row[$key3];

            if (array_key_exists($key, $grouped)) {
                $grouped[$key] .= self::COMMA.$notKey;
            } else {
                $grouped[$key] = $notKey;
            }

            return $grouped;
        }, array());
        return $grouped;
    }

    /**
     * group byした結果をフォーマットに埋め込む.
     * @return string フォーマットした文字列.
     */
    public function groupByAndFormat(): string
    {
        $key1 = 'kuni';
        $key2 = 'todofuken';
        $key3 = 'other';
        $format = "{$key1}{$key2}の温泉には、{$key3}があります。";

        /** @var array $groups group byした結果. */
        $groups = self::groupByTwoKey($key1, $key2);

        /** @var array $formated フォーマットした文字を格納する配列. */
        $formated = array();
        /** @var string $kuni kuniカラムの値. */
        foreach ($groups as $kuni => $todofukens) {
            /** @var string $todofuken todofukenカラムの値. */
            foreach ($todofukens as $todofuken => $others) {
                /** @var string $other group byのキーにしなかった要素を「と」でつないだ文字列. */
                $other = '';
                foreach ($others as $otherParts) {
                    /** @var string $otherPart group byのキーにしなかった要素をくっつけた文字列. */
                    $otherPart = implode($otherParts);
                    if (strlen($other) !== 0) {
                        $other .= 'と';
                    }
                    $other .= $otherPart;
                }
                /** @var array 置換する文字のセット. */
                $replacePairs = [$key1 => $kuni, $key2 => $todofuken, $key3 => $other];
                $formated[] = strtr($format, $replacePairs);
            }
        }

        // 改行でつなげてⅠつの文字列にする.
        return implode(PHP_EOL, $formated);
    }
}

$groupBy = new GroupByArray();
var_dump($groupBy->groupByTwoKey('kuni','todofuken'));
var_dump($groupBy->groupByTreeKey('kuni','todofuken', 'shi'));
var_dump($groupBy->groupByAndFormat());
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4