カヤック発OSSカタログ

SREチームの長田です。

今回は、カヤックのメンバーが業務で使うために開発・公開しているOSSなプロダクトをまとめて紹介しようという企画です。 KAYAC organization以下にあるものだけでなく、在籍中のメンバーが作ったものもひっくるめて、実際に業務で使用しているものを中心に 紹介しています。

以下の3つのカテゴリに分けて記載しています。 各カテゴリ内はアルファベット順です。

  • ツール編
    • 人間が手動で実行するもの
  • アプリケーション編
    • どこかに常駐して、イベントを受け取ると動作するもの
  • ライブラリ編
    • ツールやアプリケーションから参照されるもの

集めてみたらそこそこの量になったので、本記事では詳細な説明は省いています。 GitHubリポジトリのURLや関連記事のリンクを併記していますので、より詳しく知りたい場合はそちらを参照ください。

(...) 内はそのプロダクトで使用している主なプログラミング言語です。

ツール編

aws-secrets-dumper (Go)

AWS Secrets Manager / AWS Systems Manager Parameter Store の値を、Terraformで管理するためのツールです。 既存のリソースをリストアップし、 .tf ファイルとして出力します。

関連記事:

awslim (Go)

awscliのGo実装です。 Python実装の本家と比べて、数倍〜25倍速程度でコマンドを実行できます。

関連記事:

ecspresso (Go)

Amazon ECSにデプロイするためのコマンドラインツールです。 カヤックが運用するサービスはほぼこのツールでデプロイされています。

関連記事:

ecsta (Go)

Amazon ECSのタスクに関する様々な操作を行うためのコマンドラインツールです。 もともとは ecspresso にあった機能を、責務分離のために単一のツールとして切り出したもの、という経緯があります。 作者の藤原曰く、「マネコンがいらないくらい便利

iam-policy-filder (Go)

AWS Identity & Access Management(IAM)のPolicyから、特定のActionを許可するものを抽出するコマンドラインツールです。 たまにある「あるActionが廃止される」「あるActionの許可が細分化される」場合の対応範囲特定に役立ちます。

関連記事:

lambroll (Go)

AWS Lambda版ecspressoとも言うべきデプロイツール。 ソースコードの圧縮・アップロード・エイリアス登録など、デプロイに必要な一連の操作が1コマンドで完結します。

関連記事:

setddblock (Go)

DynamoDBでロック管理を行う、分散ロックツール。 分散環境での定期実行処理を走らせる場合の排他制御などに力を発揮します。

ssmwrap (Go)

AWS Systems ManagerのParameter Storeに保存された値を環境変数・ファイルにエクスポートした上で任意のコマンドを実行するコマンドラインツールです。 環境変数・ファイルという一般的なインターフェイスを利用し、手軽に・手離れ良くParameter Storeを使用することができます。 Goで書かれたアプリケーションからライブラリとして利用することも可能です。

関連記事:

tracer (Go)

Amazon ECSの1タスクに関連するログを時系列順に表示するコマンドラインツールです。 「異常終了したタスクの最後のログをtracerで取得しSlackに投げる」仕組みを作ると、原因調査のハードルがぐっと下がるのでおすすめです。

アプリケーション編

cognito-gate (Go)

Amazon Cognitoで認証をかける際の、認証前Lambdaトリガーとして使用するアプリケーションです。 ユーザーのメールアドレスを見て、完全一致・ドメイン部一致パターンを設定するだけで手軽に動作させることができます。

gdnotify (Go)

Google Drive内の変更を、Amazon EventBridge経由で受け取るためのアプリケーションで、Lambda functionで動作させることを前提としています。 スプレッドシートの変更をトリガーに、何かしらの自動処理をAWS上で実行する際に利用しています。

katsubushi (Go)

分散環境でユニークなIDを発番するアプリケーションです。 ID発番をデータベースに任せたくない場合、例えば水平分割したデータベースにレコードを挿入する場合などに使用しています。 Goのライブラリとしても使用できます。

関連記事:

lamux (Go)

リクエストに使用したドメインの、サブドメイン部分に対応するLambda functionのエイリアスをinvokeします。 Lambda functionで動かすアプリケーションを開発している際に、異なるバージョンのアプリケーションを並行して動作確認する際に使用しています。 後に紹介するmirage-ecsのAmazon Lambda版とも言えるでしょう。

let-rds-sleep (Go)

Amazon RDSのインスタンス・クラスタを1週間以上止めておくためのLambda functionです。 たまにしか使用しないけど、都度バックアップからリストアするのはめんどくさい・・・ときに使います。

関連記事:

mirage-ecs (Go)

Amazon ECSの 1Task=1環境 としてサクッと結合テスト環境を起動するアプリケーション。 自分専用の環境をサクッと起動できるので、開発体験が大幅に向上します。

prepalert (Go)

サーバ監視サービスMackerelのアラート発報に、関連情報を追加記録するためのアプリケーションです。 きちんと仕込めば、アラート発報時点で障害調査に必要な情報はすでにそろっている状態にできます。

関連記事:

ライブラリ編

他のアプリケーションやツールから利用されるライブラリ類。

ActiveRecord::CursorPager (Ruby)

ActiveRecordで、ページネーションを行うためのライブラリです。 ページ番号ではなく、前後のカーソルを指定することで、ページネーションを実現します。 地域通貨サービスまちのコインで使用されています。

ActiveAdmin::MenuTree (Ruby)

Ruby on Rails用の管理画面フレームワーク ActiveAdmin の、グローバルメニューをカスタマイズするためのRuby gemです。 移住支援サービスSMOUTの運用管理画面で実際に使用されています。

関連記事:

canyon (Go)

Amazon SQSをバックエンドとしたjob queue workerを実装するためのGoライブラリです。 AWS Lambdaで受け付けたリクエストの実処理をバックエンドに回す際などに利用するとよいでしょう。

ecsmeta (Go)

Goで書かれたアプリケーション内から、Amazon ECSメタデータを手軽に利用するためのライブラリです。

fluent plugin各種 (Ruby)

Fluentdのプラグイン群です。 社内でのアプリケーション実行環境として主流であるAmazon ECSでは、設定一つでAmazon CloudWatch Logsにログを送信・集約できますが、ログの量が多い場合はFluentdを採用する事が多いです(主にコストの関係上)。

関連記事:

jsonreplace (Go)

JSON Schemaにマッチした部分だけを置換するGoライブラリです。 「このログ、値としてはintが正しいんだけどstringとして出力されてるんだよなー」という場合に使います。

go-config (Go)

環境変数をyaml/json/toml内から参照する機能を提供するGoライブラリです。 後に紹介するtfstate-lookupの機能も内蔵されています。 CLIインターフェイスもあるので、出力されたyaml/jsonファイルを経由すればGo以外の言語からも利用できます。

カヤックの自社サービスのうち、Goで書かれたもののほとんどで採用されています。

go-genddl (Go)

Goのstructによるテーブル定義からデータベースのスキーマを生成するツールです。 MySQLとSQLite3に対応しています。

関連記事:

go-sqlla (Go)

Go用のクエリビルダです。 スキーマ定義からコード生成されたインターフェイスを用いて、型安全にクエリを組み立てることができます。 こちらもカヤックの自社サービスの多くで採用されています。

関連記事:

slogutils (Go)

Go言語の標準ライブラリである log/slog の、Middlewareとして使用するライブラリで、ログレベルに応じた色付けを行います。 hashicorp/logutilsのように、ログメッセージ中の文字列 [INFO] [WARN] などを元にログレベルを判別する機能もあり、 log から log/slog へ移行する際にも利用できます。

tfstate-lookup (Go)

Terraformの状態管理ファイルであるterraform.tfstateファイルを参照し、管理対象リソースの情報を取得するためのGoライブラリです。 コード中から、Terraform管理の名前でリソース情報を取得することができます。 CLIツールも提供されており、単独でも利用できます。

今回紹介したOSSライブラリの中でも、社内外含めて多くのプロダクトで使われています。

まとめ

社内で「OSSカタログ的なものがあるといいよね〜」という話題があり、今回ブログ記事としてまとめてみた次第です。 もともとは社内ドキュメントに記載していたものですが、オープンソースなプロダクトの話しなので、カタログもオープンでいいだろうということで本ブログにて公開するに至りました。

今回は全部で26のOSSプロダクトを紹介しましたが、ニッチ過ぎて説明に困るものや、すでに退職したメンバーが在職中に公開したものなども含めるとまだまだあります。 紹介したものも私の観測範囲にあったもの+社内からゆるく募集したものだけなので、総数はさらに増えそうです。

ちなみに、本記事で紹介したOSSは、ほとんどが Goで書かれたもの でした。 社内のプロダクトでもGoが頻繁に使われているというのもありますが、ビルドすればポータブルになることがCLIツールを書く際に好まれているようです。

カヤックのエンジニアが解決したかった課題と、またそれらを解決する手段として作ってきたOSSプロダクトにより、カヤックのことを知っていただければ幸いです。

カヤックではOSSが好きなエンジニアを募集しています!

【JS体操】第3問「Zalgo Text の生成」〜いろんな回答紹介編〜

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

『JS体操』第3問 「Zalgo Text の生成」、今回もたくさんの方が挑戦してくださいました。ありがとうございます!本記事はその解説記事の第1弾。挑戦してくださったみなさまの回答、JS体操 QA チームが事前に検証・想定していた回答を一挙にご紹介します。

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

JS体操第3問「Zalgo Text の生成」
JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com


今回の第3問では事前にヒント記事を4件書いてみました。ぜひ併せてご覧ください。

techblog.kayac.com

techblog.kayac.com

techblog.kayac.com

techblog.kayac.com


目次


本戦上位3名の回答

本戦(延長戦前)の上位3名は以下のみなさまでした。1位の halwhite さんは3連覇ですね!CONGRATULATIONS♪


🥇 114文字 by halwhite さん

export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8))

再帰を使ったとてもシンプル&スマートな回答。
この方法は運営側では想定できていませんでした。こんなアプローチがあったとは。感激です。
ちなみに再帰を使う回答はこの1件のみ。UNBELIEVABLE♪


🥈 117文字 by ほーく さん

export default(s,r=Math.random)=>s.replace(/./g,c=>{for(s=r()*8;0<s--;)c+=String.fromCharCode(r()*112+768);return c})

素直に for 文を使ったほうが短くなるケースもあるのですね!ループを逆から回していたり、不要になった引数 s を再利用している点もさすがです。AMAZING♪


🥉 119文字 by すぎゃーん さん

export default(s,r=_=>0|Math.random()*112+768)=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(r()%8+1)].map(r)))

「1文字以上8文字以下の結合文字を付与する」という要件と「1128 の倍数である」ことをうまく利用していますね!Array コンストラクタとスプレッド構文を組み合わせ、短い文字数で密な配列を生成している点も AMAZING♪



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

114文字 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768|Math.random()*112,...Array(s()%8)].map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array(s()&7)].map(s)))

出題時点で想定していた最短文字数は halwhite さんの回答と同じく114文字でした。ロジックは3位のすぎゃーんさんの回答とほぼ同じ。+1 の回避方法がポイントです。
(ところがその後114文字からさらに短くできたとは、、、)



延長戦の回答

まず、halwhite さんの再帰を使うアプローチが想定外でした。そして再帰を使うと、想定最短文字数の114文字より更に短くできることが判明。そこで急遽延長戦を行うことにしました。詳細は以下の記事をご覧ください。

JS体操第3問「Zalgo Text の生成」

techblog.kayac.com


🥇 111文字 by halwhite さん

export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+r()*112):c)(r()*8))

延長戦開始直後、みごと自己記録更新!元の114文字のコードと見比べるとその発想の転換が素晴らしい。これは私も思いつきませんでした。当初社内では112文字が限界かなと思っていたのでみんなでびっくりしました。


halwhite さんの本戦1位の114文字を社内 QA チーム総動員で短くしようとした回答

再帰を使うアプローチは想定外だったので、もしや114文字からさらに短く出来るのでは?と社内 QA チーム総動員で検証しました。その回答をいくつかご紹介します。


114文字

export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1/8):'')=>s.replace(/./g,c=>c+z(r()))

r()*8r() にし、n-1n-1/8 にしてみましたが文字数は変わりませんでした。要件が「8文字以下」ではなく「10文字以下」だったら n-1/8n-.1 にできて1文字減らせましたね!


113文字 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n>0?String.fromCharCode(768+s()*112)+c(n-1):'')(s()*8))

不要な変数(引数)sc を再利用して1文字減らしました。



halwhite さんの延長戦111文字を社内 QA チーム総動員で短くした回答

延長戦では112文字が限界かな〜と思っていたら、halwhite さんがなんと111文字を達成。そのロジックも想定外だったのでまた慌てて社内 QA チーム総動員で検証しました。

110文字 by 社内 QA チーム

export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>~--n?s(n)+String.fromCharCode(768+r()*112):c)(r()*8))

n>0n-1~--n にまとめて1文字減らしました。


103文字 by 社内 QA チーム

本来はテストで封じたかった若干結果が怪しい回答ですが参考までに。

export default s=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+n*14):c)(Math.random()*8))

Math.random() * 112Math.random() * 8 が含まれている、つまり 1128 の倍数であることを利用しています。 テストには通りますが見た目でも若干規則性が感じられますね。
(もし出題時にこのアプローチに気づいていたらテストで封じたかった。。)


おまけ:String.fromCharCode() / String.fromCodePoint() を使わない回答

String.fromCharCode()String.fromCodePoint() は文字数が多いので、これを使わずに済めば、、とも考えられます。なお Unicode のコードポイントを文字列に変換する方法は以下の記事で検証していますのでぜひご覧ください。 techblog.kayac.com


135文字 eval() + Unicode エスケープシーケンス by 社内 QA チーム

激重注意!

export default s=>s.replace(/./g,c=>c+eval(`''`+`+eval('"\\\\'+'u{'+(768+s()*112|0).toString(16)+'}"')`.repeat(1+(s=Math.random)()*8)))

eval() を2重に使うので激重です。そして文字数はむしろ長くなってしまいました。テストに本当にものすごく時間がかかるのでお試しいただく際は要注意です。


129文字 unescape() を使う回答 by 社内 QA チーム

export default s=>unescape(s.replace(s=/./g,c=>c+c.padEnd((c=Math.random)()*8+1).replace(s,_=>'%u0'+(768|c()*112).toString(16))))

懐かしの unescape() と「%エンコード」で。素直に String.fromCharCode() を使うほうが短くできそうです。


おまけ:eval() + String.fromCharCode() シリーズ

115文字 eval() を使う回答 by 社内 QA チーム

export default s=>s.replace(/./g,c=>c+eval(`String.fromCharCode(${"768+s()*112,".repeat(1+(s=Math.random)()*8)})`))

こんな回答も出題時に想定していました。こちらもテストにものすごく時間がかかるのでお試しいただく際は要注意です。


113文字 eval() を使う回答 by 社内 QA チーム

export default(s,r=Math.random)=>s.replace(/./g,c=>eval('c'+'+String.fromCharCode(768+r()*112)'.repeat(1+r()*8)))

eval() での115文字の回答を、延長戦の期間に113文字にできました。こちらもテストにものすごく時間がかかります。



おまけ:NG 集

Math.random() 以外の方法でランダムっぽい数値を生成するのも面白いテーマですね。今回はテストを厳しめにして封じてしまいましたが、供養のためにいくつかご紹介します。


121文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答①

export default(g=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=g++,...Array(g%8)].map(_=>768+((s^=s*4)>>>0)%112))))``


119文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答②

export default(n=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(-~(n=n*9%8))].map(_=>768+(n=n*9%8)*14))))(.7)


114文字 Math.random() を使わずに頑張ったがテストにぎりぎり通らない回答③

export default((i,j=i)=>s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(1+i++%8)].map(_=>768+j++%112))))``


いろんな回答一覧

最後に上で紹介しきれなかったものも含め、上位3名の方の回答とそれを元に QA チームで検証した回答、そして元々想定していた回答を文字数順にたくさん載せておきます。
eval() を使うものなど、処理が重い回答もあるので注意してください)

export default(s,r=Math.random,z=n=>~n?String.fromCharCode(768+r()*112)+z(n-1):String.fromCharCode(768+r()*112)+'')=>s.replace(/./g,c=>c+z(r()*8-2))
export default(s,r=_=>768|Math.random()*112,z=n=>~n?String.fromCharCode(r())+z(n-1):String.fromCharCode(r())+'')=>s.replace(/./g,c=>c+z(r()%8-1))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u{${(768+s()*112|0).toString(16)}}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${c.padEnd(1+8*(c=16,s=Math.random)()).replace(/./g,_=>`\\u0${(s()*c*7|c*c*3).toString(c)}`)}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u0${(768+s()*112|0).toString(16)}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${Array(1+(s=Math.random)()*8|0).fill().map(_=>`\\u0${(s()*112|768).toString(16)}`).join``}"`))
export default s=>s.replace(/./g,c=>c+eval(`"${[s=Math.random,...Array(s()*8|0)].map(_=>`\\u0${(s()*112|768).toString(16)}`).join``}"`))
export default s=>s.replace(s=/./g,c=>c+eval(`"${c.padEnd(1+8*(c=Math.random)()).replace(s,_=>`\\u0${(c()*112|768).toString(16)}`)}"`))
export default s=>s.replace(/./g,c=>c+eval(`''`+`+eval('"\\\\'+'u{'+(768+s()*112|0).toString(16)+'}"')`.repeat(1+(s=Math.random)()*8)))
export default s=>unescape(s.replace(s=/./g,c=>c+c.padEnd((c=Math.random)()*8+1).replace(s,_=>'%u0'+(768|c()*112).toString(16))))
export default s=>s.replace(s=/./g,c=>c+(Array((c=_=>768|Math.random()*112)()%8+2)+'').replace(s,_=>String.fromCharCode(c())))
export default s=>s.replace(s=/./g,c=>c+(1e7+'').slice(8*(c=Math.random)()).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array((s()-768)/14|0)].map(s)))
export default(s,r=Math.random,z=n=>String.fromCharCode(768+r()*112)+(n>0?z(n-.125):''))=>s.replace(/./g,c=>c+z(r()-.125))
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(~n?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8-1))
export default(s,r=_=>768|Math.random()*112,z=n=>n>0?String.fromCharCode(r())+z(n-1):'')=>s.replace(/./g,c=>c+z(r()%8+1))
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(n>0?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8))
export default s=>s.replace(/./g,c=>c+String.fromCharCode((s=_=>768|112*Math.random())(),...Array(s()%8).fill().map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...Array(-~(8*(s=Math.random)())).fill().map(_=>768|112*s())))
export default s=>s.replace(s=/./g,c=>c+`${c=Math.random,10**(8*c()|0)}`.replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>Math.random()*8,...Array(s()|0)].map(_=>768+s()*14)))
export default(s,r=_=>768|Math.random()*112,z=n=>n>-1?String.fromCharCode(r())+z(n-1):'')=>s.replace(/./g,c=>c+z(r()&7))
export default(s,i,r=Math.random)=>s.replace(/./g,c=>{for(i=r()*8;0<i--;)c+=String.fromCharCode(r()*112+768);return c})
export default(s,r=_=>768|Math.random()*112,z=n=>String.fromCharCode(r())+(n?z(n-1):''))=>s.replace(/./g,c=>c+z(r()%8))
export default s=>s.replace(s=/./g,c=>c+c.padEnd(1+8*(c=Math.random)()).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(s=/./g,c=>c+c.repeat((c=Math.random)()*8+1).replace(s,_=>String.fromCharCode(768|112*c())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[...c.padEnd(1+8*(s=Math.random)())].map(_=>768|112*s())))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[...Array(1+(s=Math.random)()*8|0)].map(_=>768+s()*112)))
export default(s,r=_=>768|Math.random()*112)=>s.replace(/./g,c=>c+(c=n=>~n?String.fromCharCode(r())+c(n-1):'')(r()&7))
export default(s,z)=>s.replace(/./g,c=>c+(z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')(~(s=Math.random,s()*8)))
export default(s,r=Math.random,z=_=>s--?String.fromCharCode(768+r()*112)+z():'')=>s.replace(/./g,c=>c+z(s=r()*8+1|0))
export default(s,r=Math.random,z=_=>String.fromCharCode(768+r()*112)+(s--?z():''))=>s.replace(/./g,c=>c+z(s=r()*8|0))
export default(s,r=Math.random,z=n=>n>-1?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8-1))
export default(s,r=Math.random,f=(n=r()*8)=>n>0?String.fromCharCode(768|r()*112)+f(n-1):'')=>s.replace(/./g,c=>c+f())
export default(s,f=(n=(s=Math.random)()*8)=>n>0?String.fromCharCode(768|s()*112)+f(n-1):'')=>s.replace(/./g,c=>c+f())
export default s=>s.replace(/./g,c=>c+(s=_=>768|Math.random()*112,c=n=>~n?String.fromCharCode(s())+c(n-1):'')(s()&7))
export default(s,r=Math.random)=>s.replace(/./g,c=>{for(s=r()*8;0<s--;)c+=String.fromCharCode(r()*112+768);return c})
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=Math.random,...Array(s()*8|0)].map(_=>768+s()*112)))
export default s=>s.replace(/./g,c=>c+(c=(n=(s=Math.random)()*8)=>n>0?String.fromCharCode(768|s()*112)+c(n-1):'')())
export default(s,r=Math.random,z=n=>n--?String.fromCharCode(768+r()*112)+z(n):'')=>s.replace(/./g,c=>c+z(1+r()*8|0))
export default(s,r=Math.random,z=n=>String.fromCharCode(768+r()*112)+(n--?z(n):''))=>s.replace(/./g,c=>c+z(r()*8|0))
export default s=>s.replace(/./g,c=>c+eval(`String.fromCharCode(${"768+s()*112,".repeat(1+(s=Math.random)()*8)})`))
export default(s,z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~(s=Math.random,s()*8)))
export default(s,z=n=>n++?String.fromCharCode(768+s()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~((s=Math.random)()*8)))
export default(s,r=Math.random,z=n=>n++?String.fromCharCode(768+r()*112)+z(n):'')=>s.replace(/./g,c=>c+z(~(r()*8)))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-.125):'')=>s.replace(/./g,c=>c+z(r()))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1/8):'')=>s.replace(/./g,c=>c+z(r()))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768|Math.random()*112,...Array(s()%8)].map(s)))
export default s=>s.replace(/./g,c=>c+String.fromCharCode(...[s=_=>768+Math.random()*112,...Array(s()&7)].map(s)))
export default(s,r=Math.random,z=n=>n>0?String.fromCharCode(768+r()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z(r()*8))
export default(s,z=n=>n>0?String.fromCharCode(768+s()*112)+z(n-1):'')=>s.replace(/./g,c=>c+z((s=Math.random)()*8))
export default s=>s.replace(/./g,c=>c+(c=n=>n++?String.fromCharCode(768+s()*112)+c(n):'')(~(s=Math.random,s()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n++?String.fromCharCode(768+s()*112)+c(n):'')(~(s()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>n>0?String.fromCharCode(768+s()*112)+c(n-1):'')(s()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>eval('c'+'+String.fromCharCode(768+r()*112)'.repeat(1+r()*8)))
export default s=>s.replace(/./g,c=>c+(s=Math.random,c=n=>~--n?String.fromCharCode(768+s()*112)+c(n):'')(s()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-.125)+String.fromCharCode(768+r()*112):c)(r()))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+r()*112):c)(r()*8))
export default(s,r=Math.random)=>s.replace(/./g,c=>(s=n=>~--n?s(n)+String.fromCharCode(768+r()*112):c)(r()*8))
export default s=>s.replace(/./g,c=>c+(c=n=>n>0?String.fromCharCode(768+14*n)+c(n-1):'')(Math.random()*8))
export default s=>s.replace(/./g,c=>(s=n=>n>0?s(n-1)+String.fromCharCode(768+n*14):c)(Math.random()*8))



まとめ

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

今回の第3問、想定していた最短文字数がどんどん更新されていくのにとても驚きましたし楽しかったです。再帰のアプローチを見たときにはうぉぉ、と感動しました。 1位の halwhite さんは第1問の44文字、第2問(正攻法)の66文字、今回第3問本戦の114文字、延長戦の111文字、全てで1位です!すごすぎます。

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

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

次回の記事は第3問の解説第2弾。
いくつかの回答の詳しい解説と、今回の問題の応用例などを紹介します。
ぜひご期待ください。



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

登録フォーム

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

hubspot.kayac.com

技術部公式 X アカウント

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

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


『JS体操』過去問一覧

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

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


お知らせ

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

techblog.kayac.com

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

www.kayac.com www.kayac.com