ツイート貼りすぎてページロード重いですがご容赦ください
問題
@Yametaro さんメインで定期開催していた #ゆめみからの挑戦状 ですが,今回は私から PHP に関連する問題として出題させていただきました。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
// 以下と同じ配列を $in から作って出力してください
//
// [
// '1st' => 'one',
// '2nd' => 'two',
// '3rd' => 'three',
// '4th' => 'four',
// '5th' => 'five',
// '6th' => 'six',
// '7th' => 'seven',
// '8th' => 'eight',
// '9th' => 'nine',
// '10th' => 'ten',
// '11th' => 'eleven',
// '12th' => 'twelve',
// '13th' => 'thirteen',
// '14th' => 'fourteen',
// '15th' => 'fifteen',
// ]
直近が壮大な大喜利問題でしたが,今回は割と真面目です😂
難易度を極端に上げてもアレなので,「あんまり PHP に詳しくなくても一応解ける」ぐらいに調整しました。
「数値添字配列」と「連想配列」が区別されずにぐっちゃぐちゃになる PHP らしい問題です。
(内部的には速度最適化のために区別されているらしいですが,ユーザからは見えにくい部分です)
思考のポイント
今回問われる観点は以下の 4 つです。
- どのように再帰もしくは平坦化するか
- どのように
1st
2nd
3rd
のような数字付き省略形序数詞を判定するか - どのように値とキーを反転させるか
- どのようにソートさせるか
よく見られたパターン・個人的に紹介したいパターンについて記載します。
面倒なので今回はマニュアルとかへのリンクは割愛します
どのように再帰もしくは平坦化するか
array_walk_recursive
に任せる
一番 PHP らしい方法ですね。圧倒的にこれを使った解答が多かったです。
「ネストしている末端部分のキーバリューペアのみを取りたい」
その要求をドンピシャで叶えてくれます。但し,新しい配列をリターンするのではなく,既存配列を手続き的に書き換えるデザインとなっており,新しい変数に入れるためにはコールバックに use
での参照変数が必要になるなど,微妙に癖が強い部分はありますね。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
$out[$key] = $value;
});
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"2nd": "two",
"four": "4th",
"three": "3rd",
"one": "1st",
"10th": "ten",
"6th": "six",
"5th": "five",
"seven": "7th",
"fourteen": "14th",
"11th": "eleven",
"8th": "eight",
"thirteen": "13th",
"12th": "twelve",
"nine": "9th",
"15th": "fifteen"
}
*/
RecursiveArrayIterator
RecursiveIteratorIterator
に任せる
array_walk_recursive
の設計上の問題点を克服し,値をリターンする使い方が出来る方法です。 PHP 標準の再帰イテレータの機能を使っています。 これもそこそこ使用者が居たように見えました。
PHP 8.1 から ...
がイテレータの展開に対応し,めちゃくちゃ気持ちよく書けるようになりました。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
// PHP 8.1 からはなんとこれでいい!
$out = [...new RecursiveIteratorIterator(new RecursiveArrayIterator($in))];
// PHP 8.0 まではこう
// - 第 2 引数に false を渡すと array_merge っぽいキー合成になる (... と同じ)
// - 第 2 引数に true を渡すと array_replace っぽいキー合成になる
// 今回は文字列キーしかないのでどちらでも同じ
//
// $out = iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator($in)), false);
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"2nd": "two",
"four": "4th",
"three": "3rd",
"one": "1st",
"10th": "ten",
"6th": "six",
"5th": "five",
"seven": "7th",
"fourteen": "14th",
"11th": "eleven",
"8th": "eight",
"thirteen": "13th",
"12th": "twelve",
"nine": "9th",
"15th": "fifteen"
}
*/
array_filter
array_replace
を使った合成
今回は「無限にネストした配列に対応できるようにしてください」とは一言も言っていません。1段階ネストまでならこれで簡単に捌けます。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
$out = array_replace(array_filter($in, 'is_scalar'), ...array_filter($in, 'is_array'));
echo json_encode($out, JSON_PRETTY_PRINT);
/*
さっきまでとは少し順番が違います
{
"three": "3rd",
"10th": "ten",
"5th": "five",
"seven": "7th",
"thirteen": "13th",
"12th": "twelve",
"nine": "9th",
"2nd": "two",
"four": "4th",
"one": "1st",
"6th": "six",
"fourteen": "14th",
"11th": "eleven",
"8th": "eight",
"15th": "fifteen"
}
*/
配列だけを抜き取ったあと 1 段階平坦化して配列以外のものとくっつける,という方法です。
実は今回自分が最初に思いついたものがこれでした。いやー最初はこれ思いついたときキレイだなって思ったんですよ!でも ...
使ったイテレータ展開の方法に長さで負けてたら立場がないわ,PHP 8.1 から強化されたの忘れてたわ🥺
foreach
による愚直なループ
説明不要。あんまりかっこよくないので,これを使った方は別の面で芸術点が欲しいところです(何の話だ)
どのように 1st
2nd
3rd
のように数字付き省略形序数詞を判定するか
1文字目が数値文字かどうかを見る
初見で度肝を抜かれました。実務だと Uninitialized string offset エラーを引くリスクがあるからあんまり使いたくはないですね。
<?php
var_dump(is_numeric('1st'[0])); // true
var_dump(is_numeric('first'[0])); // false
var_dump(ctype_digit('1st'[0])); // true
var_dump(ctype_digit('first'[0])); // false
preg_match
を使っている方も多数いらっしゃいましたが,このへんは PHP に精通している人なら正規表現を使わずに捌く部分ですね。
整数型にキャストする
わりとこっちがオーソドックスなんじゃないかなと思います。 以下の説明中の (bool)
キャストは省略してもいいです。
<?php
var_dump((bool)(int)('1st')); // true
var_dump((bool)(int)('first')); // false
var_dump(boolval(intval('1st'))); // true
var_dump(boolval(intval('first'))); // false
var_dump((int)'1st' > 0); // true
var_dump((int)'first' > 0); // false
// var_dump('1st' > 0); // true
// var_dump('first' > 0); // ← PHP 8.0 以降と 7.4 までで動作が変わっています!要注意
string
と int
の曖昧比較は PHP 8.0 で破壊的変更が入ったので注意してください。
ctype_alpha
の否定を使う
「全てがアルファベットではない」 → 「少なくとも数字が 1 つ以上含まれている」
<?php
var_dump(!ctype_alpha('1st')); // true
var_dump(!ctype_alpha('first')); // false
今回に限りこういう方法も使えますね。
どのように値とキーを反転させるか
array_walk_recursive
または foreach
ループの中で if または三項演算子
最も愚直な方法。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
if ((int)$value) {
$out[$value] = $key;
} else {
$out[$key] = $value;
}
});
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"2nd": "two",
"4th": "four",
"3rd": "three",
"1st": "one",
"10th": "ten",
"6th": "six",
"5th": "five",
"7th": "seven",
"14th": "fourteen",
"11th": "eleven",
"8th": "eight",
"13th": "thirteen",
"12th": "twelve",
"9th": "nine",
"15th": "fifteen"
}
*/
少しオシャレな書き方だと,こういうのもありました。
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
(int)$value ? $out[$value] = $key : $out[$key] = $value;
});
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
$out[(int)$value ? $value : $key] = (int)$value ? $key : $value;
});
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
$out += (int)$value ? [$value => $key] : [$key => $value];
});
亜種1: min
max
で比較する
こちらは解答からピックアップ。初見でかなりびっくりしました。少し分かりやすいように書き下します。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
$out = [];
array_walk_recursive($in, function (...$pair) use (&$out) {
$out[min($pair)] = max($pair);
});
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"2nd": "two",
"4th": "four",
"3rd": "three",
"1st": "one",
"10th": "ten",
"6th": "six",
"5th": "five",
"7th": "seven",
"14th": "fourteen",
"11th": "eleven",
"8th": "eight",
"13th": "thirteen",
"12th": "twelve",
"9th": "nine",
"15th": "fifteen"
}
*/
なるほど, 数字がアルファベットよりも文字コードが若い ことを利用しているんですね!これは賢い
ゴルフ専用なのでプロダクションでは使えませんが,なかなかエレガントだと思いました。
亜種2: 引数を sort
する
@tadsan 氏の解答自体はこの記事最後の大喜利部門で取り上げているのですが,他にも工夫点があって見落としていたので追記で紹介します。
<?php
$in = [
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
];
$out = [];
array_walk_recursive($in, function (...$pair) use (&$out) {
sort($pair);
$out[$pair[0]] = $pair[1];
});
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"2nd": "two",
"4th": "four",
"3rd": "three",
"1st": "one",
"10th": "ten",
"6th": "six",
"5th": "five",
"7th": "seven",
"14th": "fourteen",
"11th": "eleven",
"8th": "eight",
"13th": "thirteen",
"12th": "twelve",
"9th": "nine",
"15th": "fifteen"
}
*/
考え方は min
max
と同じですが,(最後のステップでソートを使うのに)こんなところでもソートが出てくるのが面白いですね。
平坦化済みの配列に対して array_flip
を適用する
array_flip
という関数はエントリ全体の反転を行ってくれます。それ故に,反転してほしいものだけをまとめて取り出して後から合成という手法を採ることができます。
<?php
$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
]))];
// 値が 1st, 2nd, ... のものだけを取り出してキー反転
// 値が first, second, ... のものだけを取り出す
// これらを合成
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"4th": "four",
"3rd": "three",
"1st": "one",
"7th": "seven",
"14th": "fourteen",
"13th": "thirteen",
"9th": "nine",
"2nd": "two",
"10th": "ten",
"6th": "six",
"5th": "five",
"11th": "eleven",
"8th": "eight",
"12th": "twelve",
"15th": "fifteen"
}
*/
どのようにソートさせるか
ksort
で SORT_REGULAR
SORT_NATURAL
を指定
PHP のソート関数,めちゃくちゃ大量にあって選ぶのに困りますが, ちょうど ksort()
がキーを基準としてキーバリューペアをソートする関数になります。しかし,そのままでは 10th
以上の値の順序がおかしくなってしまいます。 文字列では "1" "10" "2"
という順序になるためです。
<?php
$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
]))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');
ksort($out);
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"10th": "ten",
"11th": "eleven",
"12th": "twelve",
"13th": "thirteen",
"14th": "fourteen",
"15th": "fifteen",
"1st": "one",
"2nd": "two",
"3rd": "three",
"4th": "four",
"5th": "five",
"6th": "six",
"7th": "seven",
"8th": "eight",
"9th": "nine"
}
*/
そこで, SORT_NUMERIC
SORT_NATURAL
いずれかのフラグをつけると比較方法を変えることができ,この問題に対処できます。
フラグ | 方式 |
---|---|
SORT_NUMERIC |
(int) キャストされた状態でソート |
SORT_NATURAL |
strnatcmp の自然順比較でソート |
<?php
$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
['2nd' => 'two', 'four' => '4th'],
'three' => '3rd',
['one' => '1st'],
'10th' => 'ten',
['6th' => 'six'],
'5th' => 'five',
'seven' => '7th',
['fourteen' => '14th', '11th' => 'eleven'],
['8th' => 'eight'],
'thirteen' => '13th',
'12th' => 'twelve',
'nine' => '9th',
['15th' => 'fifteen'],
]))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');
ksort($out, SORT_NUMERIC);
echo json_encode($out, JSON_PRETTY_PRINT);
ksort($out, SORT_NATURAL);
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
"1st": "one",
"2nd": "two",
"3rd": "three",
"4th": "four",
"5th": "five",
"6th": "six",
"7th": "seven",
"8th": "eight",
"9th": "nine",
"10th": "ten",
"11th": "eleven",
"12th": "twelve",
"13th": "thirteen",
"14th": "fourteen",
"15th": "fifteen"
}
*/
ほか, uksort
でコールバック関数として "strnatcmp"
を指定する方法もあります。 SORT_NATURAL
を使った方法と動作的には全く同じです。
uksort($out, 'strnatcmp');
解答紹介
模範解答以外は大喜利メインでいきます。
問題作成者推奨解答
$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator($in))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');
ksort($out, SORT_NATURAL);
一般的部門 (+大喜利になりきれなかった部門)
愚直な考え方も大事です
1行に押し込めばいいって問題じゃないよ!
上に同じ。 ctype_alpha
に気づいたところはいいね!
array_walk_recursive
部門なら模範解答
配列で +=
操作してくれる人好きです
配列結合演算子, array_merge, array_replace を徹底比較 - Qiita
$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($in));
$tmp = array_fill(1, iterator_count($iterator), "");
foreach($iterator as $key => $value) {
if (preg_match("/^([0-9]+)/", $key, $match) === 1) {
$index = $match[1];
$counter = $key;
$number = $value;
} else {
preg_match("/^([0-9]+)/", $value, $match);
$index = $match[1];
$counter = $value;
$number = $key;
}
$tmp[$index] = [$counter => $number];
}
$result = array_reduce($tmp, "array_merge", []);
var_dump($result);
こういうのバケツソートって言うんですよね
でぃーほりさんが初めて array_flip
使った解答出してくれたので嬉しかった
array_walk_recursive
を限界まで削った形
読みにくいけどやってることは普通なので一般部門! array_reduce
使った解答はこれが初観測
match
式は好きです
$it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($in));
$flatNum = iterator_to_array(new \RegexIterator($it, '/^\d/'));
$flatAlpha = iterator_to_array(new \RegexIterator($it, '/^[^\d]/'));
$out = array_merge(array_flip($flatNum), $flatAlpha);
ksort($out, SORT_NATURAL);
var_dump($out);
RegexIterator
に芸術点を加算
大喜利部門
モダン PHP をぶっ壊す!!!
唐突な strtotime
見たとき腹抱えて笑いました。わざわざ序数詞を日付にしなくてもどうにかなるよw
こういう問題出すとソースコードコメント解析して解答を eval 錬成するマン絶対おるんよ
ドMかな?
$ys = [];
foreach ($in as $k => $xs)
if ([] < $xs)
foreach ($xs as $l => $x)
$ys[(int)[$x, $l][$l < $x]] = [[$x, $l], [$l, $x]][$l < $x];
else
$ys[(int)[$xs, $k][$k < $xs]] = [[$xs, $k], [$k, $xs]][$k < $xs];
$zs = [];
for ($i = true; $ys[$i] ?? false; $i += true)
$zs[$ys[$i][false]] = $ys[$i][true];
echo "[\n";
foreach ($zs as $k => $z) {
echo " '{$k}' => '{$z}',\n";
}
echo "]\n";
別方面のドM。これはすごい。 関数コールも数値リテラルも 1 つもないようです。 これは PHP を熟知していないと出来ない技。
「$in
を使って」の条件は満たしているので合格です
全部 json_encode()
してパースマンが数人居ました。 json_decode()
職人してる人は加点します
preg_match_all('/"(\d+)\w+"/', $json = json_encode($in), $m);
$out = [];
$date = new DateTimeImmutable;
for($i = max(1, min($m[1])), $max = min(31, max($m[1])); $i <= $max; $i++) {
$key = $date->setDate(2022, 1, $i)->format('jS');
if(preg_match('/("(\w+)":)?"'.$key.'"(:"(\w+)")?/', $json, $m) && $value = $m[2] ?: $m[4]) {
$out[$key] = $value;
}
}
print_r($out);
JSON + 日付職人の合わせ技。これはすごい
ob_start()
+ ob_get_clean()
,この上ないぐらい PHP らしくて好き
こちらは var_export()
派
http_build_query()
派
ごめん読めない
配列と可変長引数の交換が忙しすぎる
SplPriorityQueue
!久しぶりに名前聞いたな
シェル芸を我慢できなかった人
こちらもただのシェル芸…かと思ったら,よく見たら スリープソート の実装でした。プロセス開いてるほうがインパクト強すぎて最初気づかなかったw
PHP はプロセス開かないと JavaScript みたいな遅延処理を実現できないんだなぁ…
乱数のシードをわざわざ見つけてきた人。 これが一番びっくりした
$in
をどっかに忘れてきた人たち。 @rana_kualu さんのはさすがに草
職場部門
表彰
優勝
@akebi_mh さん
めっちゃいっぱい回答してくれて感激です。 min()
max()
の交換が目からウロコでした。
準優勝(追加)
@tadsan さん
せっかくスリープソートで実装してくれたので追加で表彰します!
(最初気づかなくてすみません
ナイスゴルフ賞
@rana_kualu さん
ゴルフといえばこの方の印象。問題文読んでない人他にもたくさんいたので細かいことは気にしないです(気にしろよ)
ナイスアルゴリズム賞
@tyabu12 さん
乱数調整に度肝を抜かれました。
PHP エキスパート賞(追加)
@nsfisis さん
難易度の高い縛りプレイに拍手。
難易度高いなぁと改めて思ったので追加しました!