【JS体操】第4問「ひらがなを画数順に並び替えよう」解説(最短文字数は109文字!)

こんにちは!面白プロデュース事業部のおばらです。

大変お待たせいたしました!!
『JS体操』第4問「ひらがなを画数順に並び替えよう」の解説記事をお届けいたします。

今回もたくさんの方に挑戦していただきありがとうございます!
本記事では挑戦してくださったみなさまの回答、JS体操 QA チームが事前に検証・想定していた回答を一挙にご紹介します。

もし第4問まだ挑戦できていなかった!というひとは以下よりぜひ。

hubspot.kayac.com

目次


上位3名の回答

第4問の上位3名は以下のみなさまでした。CONGRATULATIONS♪


🥇 109文字 by ksk1015 さん

export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>[...s.matchAll(c)].join``)

いやー今回も1位の回答(ロジック)は運営側で想定できていませんでした。こんなアプローチがあったとは。感激です。
UNBELIEVABLE♪

このあと詳しく解説しますのでお楽しみに。


🥈 112文字 by halwhite さん

export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>s.replace(/./g,d=>d==c?c:''))

1位の ksk1015 さんの回答と同じロジックですね!流石です。
AMAZING♪



🥉 120文字 by ほーく さん

export default s=>[...s].sort((a,b,f=c=>"しつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ".indexOf(c))=>f(a)-f(b)).join("")

ほーくさんの回答はもともと想定していた110文字のロジック近いものでした!
EXCELLENT♪



社内で元々想定していた最短文字数の回答

export default s=>[...s].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(b+'.*'+a)).join``

正規表現を使っているのがポイントです!
こちらも詳しく解説します!




解説

要件を振り返る

まず並び替えの要件を振り返りましょう!

  • ① 画数順に並び替える
  • ② 画数が同じものは五十音順に並び替える

ここで(本問題の範囲では)「五十音順」=「Unicode のコードポイント順」であることにも注目しましょう。
つまり、上の①②は以下のように言い換えられます。

  • ① 画数順に並び替える
  • ② 画数が同じものは Unicode のコードポイント順に並び替える

①②より、任意のテストにおいて、

  • あるひらがな A
  • 別のひらがな B

のどちらが先にくるかの関係は一定であるといえます!

つまり
画数 3 の「あ」は、画数 2 の「う」の前に来ることはない
ですし、
画数 3 で、コードポイントが 12354 の「あ」は、同じく画数 3 だがコードポイントがより大きい 12363 の「か」の後ろに来ることはない
というふうに、テストによって順番が入れ替わることはないです!




順位表を作る

①②の要件で、テストに使われるすべてのひらがなを(JavaScript で)並びかえると

'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'

です!

これはつまり順位表です。
この順位表を参考に並び替えればよいです。

あるひらがな h が上記の順位表の何番目か(index)は

'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(h)

で取得できますね!




順位を返す関数を作る

あるひらがなの順位を返す関数を作っておきましょう。

const f = h => 'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(h);

あるひらがなの組み合わせ、ab を上記順位表の順位で並び替えるソート関数は

const g = (a, b) => f(a) - f(b);

これで並び替えのロジックができました。




並び替える

並び替えをするには Array クラスのメソッド sort() が便利です。
sort() は破壊的なメソッドであることに注意しましょう!非破壊のほうが都合がいい場合は toSorted() が便利です!)

sort() を使うために、文字列を1文字ずつの配列に直します。
文字列を1文字ずつの配列に変換する方法は過去に何度か紹介しているので割愛しますが
例えば以下。

const arr = [...hiragana];

これらより、まずは 241 文字!

const f = h => 'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(h);
const g = (a, b) => f(a) - f(b);

export default function sort(hiragana) {
  const arr = [...hiragana];

  arr.sort(g);

  const str = arr.join('');

  return str;
}


ざっくりコードゴルフすると、、119文字!

export default h=>[...h].sort((a,b,f=h=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(h))=>f(a)-f(b)).join``




さらに短くしてみる

ここで、String クラスの indexOf()search() に注目!

  • indexOf() は文字列を引数にとり検索するメソッド
  • search() は正規表現 or 正規表現に変換できるなにか(文字列とか)を引数に取り検索するメソッド

ですが
indexOf() よりも search() のほうがメソッド名が1文字短いですね!
なので、
indexOf()search() とし

export default h=>[...h].sort((a,b,f=h=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(h))=>f(a)-f(b)).join``

118文字で1文字減りました!




もっともっと短くしてみる

まだまだいけます!

String クラスの indexOf()search() の仕様(戻り値)に注目!
どちらも見つからなかったら -1 を返しますね!

'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(h)

だと一番小さい数値(順位)は(全部のひらがなを網羅していることが既知なので) 「く」の場合の 0 ですが、
仮に 0-1 になっても順位に影響はないですね!
つまり順位表から「く」を省略しても問題はないです。
よって、1文字減って

export default h=>[...h].sort((a,b,f=h=>'しつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(h))=>f(a)-f(b)).join``

と、ここでおそらくこの順当なアプローチの文字数の限界がやってきました。 別のアプローチができないか考えてみます!




別のアプローチを考える

先程のアプローチのロジックを今一度眺めてみましょう。

const f = h => 'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(h);
const g = (a, b) => f(a) - f(b);

f()g() でやっていることは

  • a の順位 - b の順位

です。

言い換えると、

  • a の順位が b の順位よりも大きければ正の値を返す
  • a の順位が b の順位よりも小さければ負の値を返す
  • a の順位が b の順位と同じであれば 0 を返す(つまり a === b

です。

さらに言い換えると
順位表:'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ' において、

  • ab の後ろにあれば正の値を返す
  • ab の前にあれば負の値を返す
  • a === b であれば 0 を返す

です。

ここで、
a === b の時は

  • 正の値
  • 負の値
  • 0

のどれを返しても問題ないことに注目しましょう。
同じ2つの連続する文字を、どう入れ替えても、入れ替えなくても、変わらないからです。

つまり、要件を整理すると
順位表:'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ' において、

  • ab の後ろにあれば正の値を返す
  • ab の前にあれば負の値を返す
  • a === b であれば何を返しても良い

です。

以上より

  • ab の後ろにあれば正の値を返す
  • それ以外であれば負の値を返す

でも良いです。

もちろん

  • ab の前にあれば負の値を返す
  • それ以外であれば正の値を返す

でも良いです。

(「それ以外であれば負の値を返す」のほうが例のアレが使えて楽そうな気がするので)
以下のロジックで考えてみましょう。

  • ab の後ろにあれば正の値を返す
  • それ以外であれば負の値を返す

ab の後ろにあれば

これはそれぞれの順位を比較してもよいですが、
正規表現で表せそうな要件ですね!

new RegExp(`${b}.*${a}`)\

この正規表現と String クラスの search() メソッドを使えば

  • それ以外であれば負の値を返す

も満たしてくれますね!

よって、

export default h=>[...h].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(new RegExp(`${b}.*${a}`))).join``

search(new RegExp(`${b}.*${a}`))

上述のように、search() が引数として期待しているのは正規表現ですが、正規表現に変換できるなにか(文字列とか)でも良いですね!
ということで、new RegExp() で明示的に正規表現を生成するのはやめて、文字列を渡し、暗黙的に正規表現に変換してもらっちゃいましょう!
(※ ちなみに new RegExp(`${b}.*${a}`)new は省略できます)

つまり

search(`${b}.*${a}`)

です。

さらに、
テンプレートリテラルを使うと長くなるので、 + での結合にしちゃいましょう!
つまり

search(b+'.*'+a)

すると、、

export default h=>[...h].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(b+'.*'+a)).join``

めちゃ短くなった!

とここまでが社内で想定していた最短文字数の回答でしたが、なんとこれを超える109文字の回答の方が現れましたね!
大感激です。




ksk1015 さんの回答の解説

ksk1015 さんの回答をもう一度見てみましょう!

export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>[...s.matchAll(c)].join``)

ぱっとみ何をしているのか全くわからないですね、、すごい笑
社員総出で頑張って解析したので説明します。

メインのロジックは以下です。

  • 'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ' を1文字ずつチェックし、置換していく

置換関数の処理は以下です。

  • 'くしつの...むをなほ'のうちの置換対象の1文字が、並び替え対象の文字列にマッチするかどうかをチェックし、マッチしない部分を空文字に置換して返す

以下に具体例を示します。
並び替え対象の文字列は 'あさくさ' で考えてみます。

  • く:'あさくさ''く' 1文字にマッチしたので 'く''あさくさ''く' 以外を空文字 '' に置換したもので置換
  • ... マッチなし(は空文字 '' で置換)
  • あ:'あさくさ''あ' 1文字にマッチしたので 'あ''あさくさ''あ' 以外を空文字 '' に置換したもので置換
  • ... マッチなし(は空文字 '' で置換)
  • さ:'あさくさ''さ' 2文字にマッチしたので 'さ''あさくさ''さ' 以外を空文字 '' に置換したもので置換
  • ... マッチなし(は空文字 '' で置換)

ここで、
'あさくさ''く' 以外を空文字 '' に置換したもの

[...'あさくさ'.matchAll('く')].join('') // => 'く'
であり、
'あさくさ''さ' 以外を空文字 '' に置換したもの

[...'あさくさ'.matchAll('さ')].join('') // => 'ささ'
です。

文章での説明がとても難しいですが、お分かりいただけたでしょうか!




いろんな回答一覧

最後に上で紹介しきれなかったものも含め、上位3名の方の回答とそれを元に QA チームで検証した回答、そして元々想定していた回答を文字数順にたくさん載せておきます。

export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>[...s.matchAll(c)].join``)
export default s=>[...s].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.search(b+'.*'+a)).join``
export default s=>[...s].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.match(b+'.*'+a)-1).join``
export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>s.replace(/./g,d=>d==c?c:''))
export default s=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.replace(/./g,c=>c.repeat(s.split(c).length-1))
export default s=>[...s].sort().sort((a,b,f=c=>~'くしつのへいうこてとひめりるろん_おきたぬねはふまむをなほ'.search(c)||-17)=>f(b)-f(a)).join``
export default(s,t=b=>'くしつのへいうこてとひめりるろん おきたぬねはふまむをなほ'.search(b)+1||17)=>[...s].sort().sort((a,b)=>t(a)-t(b)).join``
export default s=>[...s].sort().sort((a,b,f=c=>~'くしつのへいうこてとひめりるろんおきたぬねはふまむをなほ'.search(c)||-16.5)=>f(b)-f(a)).join``
export default s=>[...s].sort().sort((a,b,f=c=>'くしつのへいうこてとひめりるろんおきたぬねはふまむをなほ'.search(c)+1||16.5)=>f(a)-f(b)).join``
export default s=>[...s].sort((a,b,f=c=>'くしつのへいうこてとひめりるろん_おきたぬねはふまむをなほ'.search(c)+1||17)=>f(a)-f(b)||-(a<b)).join``
export default s=>[...s].sort((a,b)=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをな'.split(a)[0].indexOf(b)).join``
export default(s,t=b=>'くしつのへいうこてとひめりるろん おきたぬねはふまむをなほ'.indexOf(b)+1||17)=>s.split``.sort().sort((a,b)=>t(a)-t(b)).join``
export default s=>[...s].sort((a,b,f=c=>'しつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(c))=>f(a)-f(b)).join('')
export default s=>[...s].sort((a,b,c='しつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ')=>c.indexOf(a,c.indexOf(b))).join``
export default(s,f=c=>'しつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(c))=>[...s].sort((a,b)=>f(a)-f(b)).join('')
export default s=>[...s].sort((a,b,c='くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ')=>c.indexOf(a,c.indexOf(b))).join``
export default s=>[...s].sort((a,b)=>RegExp(b+'.*'+a).test('くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ')?1:-1).join``
export default(s,t=b=>'くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ'.indexOf(b))=>s.split``.sort((a,b)=>t(a)-t(b)).join``
export default (s,t='くしつのへいうこてとひめりるろんあえかけさすせそちにみもやゆよられわおきたぬねはふまむをなほ')=>s.split``.sort((a,b)=>t.indexOf(a)-t.indexOf(b)).join``
export default s=>[...s].sort().sort((a,b,f=c=>parseInt('くしつのへ1いうこてとひめりるろん2おきたぬねはふまむを4なほ5'.split(RegExp(c+'\\D*'))[1]||3))=>f(a)-f(b)).join``
export default s=>[...s].sort((a,b,f=c=>parseInt('くしつのへ1いうこてとひめりるろん2おきたぬねはふまむを4なほ5'.split(RegExp(`${c}\\D*`))[1]||3))=>f(a)-f(b)||-(a<b)).join``
export default s=>[...s].sort().sort((a,b,g=(c,f=b=>~(b+'').search(c))=>f`くしつのへ`?1:f`いうこてとひめりるろん`?2:f`おきたぬねはふまむを`?4:f`なほ`?5:3)=>g(a)-g(b)).join``
export default s=>[...s].sort().sort((a,b,g=(c,f=b=>~b.search(c))=>f('くしつのへ')?1:f('いうこてとひめりるろん')?2:f('おきたぬねはふまむを')?4:f('なほ')?5:3)=>g(a)-g(b)).join``
export default(h,k=(x,i='くしつへのいうこてとひめりるろんおきたぬねはふまむをなほ'.indexOf(x))=>i<0?3:i<5?1:i<16?2:i<26?4:5)=>h.split('').sort().sort((i,j)=>k(i)-k(j)).join('')
export default h=>h.split('').sort().sort((a,b,r=a=>('12233344'.repeat(5)+'233355')['くいうあえかおきしこてけさすたぬつとひせそちねはのめりにみもふまへるろやゆよむをんられわなほ'.indexOf(a)])=>r(a)-r(b)).join('')
export default s=>s.split('').sort((a,b,t=(a,b=(c,d)=>c.indexOf(d)!==-1)=>b('くしつのへ',a)?1:b('いうこてとひめりるろん',a)?2:b('おきたぬねはふまむを',a)?4:b('なほ',a)?5:3,f=c=>c.charCodeAt())=>t(a)-t(b)||f(a)-f(b)).join('')



まとめ

最後まで読んでいただきありがとうございます。
そして挑戦してくださったみなさま、ありがとうございました。

今回の第4問も想定していた最短文字数を超える、しかも想定していなかったロジックの回答にとても驚いたとともにその解析も楽しかったです。

ご紹介した様々な回答、いかがでしたか? 今回も、このアプローチは思いついてた!とか、これは思いつきそうだけど思いつけなかった!とか、こうすればあと1文字減らせたのか!など楽しめていただけていれば嬉しいです。 答えを見た後だと簡単に思いつきそうだけど、なぜか思いつかない。不思議です。それがまたコードゴルフの楽しさでもありますね。

『JS体操』はその名の通り、きつい筋トレでもなくピリピリした競技でもなく、ゆるーい頭の体操。 だからこそいろんなアプローチで解けるような楽しい問題をこれからも出題していく予定です。 第4問、第5問といまアイディアを練っていますので、お楽しみに!

次回の記事は第5問の解説記事を予定しています。
ぜひご期待ください。



『JS体操』の情報を受け取ろう

登録フォーム

次回以降の「解説ブログ」や「JS体操の問題」のお知らせが気になる方は以下のページにてご登録ください。

hubspot.kayac.com

技術部公式 X アカウント

面白法人カヤック技術部公式 X アカウント @kayac_tech でも随時情報を発信します。

カヤック技術部公式 X アカウント @kayac_tech


『JS体操』過去問一覧

『JS体操』の過去問、まだ挑戦していない!という方はぜひ。

hubspot.kayac.com hubspot.kayac.com hubspot.kayac.com hubspot.kayac.com


お知らせ

先日、Perl のコードゴルフコンテストもカヤック主催で開催されました。
こちらも面白いのでぜひご覧ください!

techblog.kayac.com

そして、カヤックではコードゴルフが大好きな新卒&中途エンジニアも募集しています!

www.kayac.com www.kayac.com