Content-Length: 156059 | pFad | http://techblog.kayac.com/#

KAYAC Engineers' Blog

【JS体操】応援ありがとうございました!〜全5問の振り返り〜

こんにちは!面白プロデュース事業部のおばらです。
本記事では JavaScript のコードゴルフ大会『JS体操』の全5問を、作問の裏話とあわせて振り返ってみよう思います!



目次


『JS体操とは』

『JS体操』とはカヤックが主催する JavaScript のコードゴルフ大会です。
もともとは社内の勉強会として始めた施策です。 その詳細は以下のブログ記事を御覧ください!

techblog.kayac.com




第1回「波形の整形」

【問題】https://hubspot.kayac.com/js-taiso-001
【解説】https://techblog.kayac.com/js-taiso-001-commentary-vol1


作問の裏話

記念すべき第1弾!

最初の問題を何にするか、とても迷いました。
運営チームでいくつか問題の候補を出して議論し、問題の意図が見た目にもわかりやすい&過去に似たような事例が少ないということで、グラフを描画する問題にしました。

グラフの形状はいろんなものを試しましたが、グラフの形が面白く、かつコードゴルフのしがいがあるもの、という観点でセレクトしました。


最短文字数の回答

なんと44文字まで縮められます!

export default x=>x-(x%=.2)+.2-(.04-x*x)**.5

以下の5名の方がみごと達成されました!

  • halwhite さん
  • koyama41 さん
  • sugyan さん
  • tkihira さん
  • たつけん さん


面白いコード

ここで tkihira さんのとても面白いアプローチの回答をご紹介します。

let r=0;export default x=>r+=x%.2/(1-(5*x%1)**2)**.5/2e5

どういうロジックか、わかりますか?
円の式を微分して、、、微小なΔx分を足していって、、、
こんなアプローチは全く思いつきませんでした。あっぱれです。
赤い点線からちょっとズレているのが御愛嬌ですね!

王道の回答以外にもいろいろ楽しんでいただけるのは出題者としても嬉しいです!




第2問

【問題】https://hubspot.kayac.com/js-taiso-002
【解説①】https://techblog.kayac.com/js-taiso-002-commentary-vol1
【解説②】https://techblog.kayac.com/js-taiso-002-commentary-vol2


作問の裏話

もともとは「縦横比」のような概念、無次元の概念を導入するとロジックをシンプルにできる!という社内勉強会用のデモでしたが、そこからとんでもないハックが可能な賛否両論の問題になってしまいました。


最短文字数の回答

真面目なアプローチだと66文字!

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b)

以下の2名のみなさん、CONGRATULATIONS♪

  • 🥇 ぺち さん
  • 🥇 halwhite さん

デフォルト引数の仕組みを利用するのがポイントですね!


鬼コード

export default()=>9e9<<length+++9

ハック部門では上記コードも想定していましたが、ちょっとやりすぎました。反省。。




第3問

【問題】https://hubspot.kayac.com/js-taiso-003
【ヒント①】https://techblog.kayac.com/generating-zalgo-text 【ヒント②】https://techblog.kayac.com/unicode-code-point-vs-code-unit 【ヒント③】https://techblog.kayac.com/12-ways-to-convert-sparse-array-to-dense-array-on-the-fly 【ヒント④】https://techblog.kayac.com/22-ways-to-generate-strings-from-code-points 【解説①】https://techblog.kayac.com/js-taiso-003-extension-match-announcement
【解説②】https://techblog.kayac.com/js-taiso-003-commentary-vol1


作問の裏話

Unicode について調べているときに思いついた問題です。こういうテキストを Zalgo Text と呼ぶこともこのとき初めて知りました。


最短文字数の回答

3連覇の halwhite さんのおかげで以下まで短くできることが判明しました!

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+eval(`''`+`+eval('"\\\\'+'u{'+(768+s()*112|0).toString(16)+'}"')`.repeat(1+(s=Math.random)()*8)))

社内の QA で生まれた、eval() を2重に使うトンデモ激重コードです。そして文字数はむしろ長くなってますね。テストに本当にものすごく時間がかかるのでお試しいただく際は要注意。




第4問

【問題】https://hubspot.kayac.com/js-taiso-004
【解説】https://techblog.kayac.com/js-taiso-004-commentary


作問の裏話

社内では、

  • 安定ソート版
  • 安定じゃないソート版

の2種類のセットで出した問題でした。
それぞれでロジックも変わってくるので面白いです。もし興味のある方はぜひお試しあれ。


最短文字数の回答

1位は ksk1015 さんの109文字でした!予想外のロジック、さすが過ぎます。

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




第5問

【問題】https://hubspot.kayac.com/js-taiso-005
【解説】https://techblog.kayac.com/js-taiso-005-commentary


作問の裏話

社内向けのバージョンでは、フリー素材と化している弊社 CBO の写真を AA にする問題でした。


Zalgo Text と AA を組み合わせる案なんてのもありました。薄いピクセルはダイアクリティカルマークを少なく、濃いピクセルはダイアクリティカルマークを多くする、という具合です。



その後、YAPC:Hakodate 2024 開催にあわせた問題にすることが決まり、

  • 北海道の地図
  • 北海道の特産物の写真
  • 「北海道」という漢字

などいろいろ考えた結果 YAPC とカヤックのロゴでいくことにしました。


最短文字数の回答

export default(t,W=128,c=new OffscreenCanvas(W,W).getContext`2d`)=>c.getImageData(~~c.drawImage(t,0,0,W,64),0,W,64).data.reduce((a,p,i)=>a+=i%4?~i%512?'':`
`:'#`'[p>>7],'')


面白い回答

いろいろ遊べるようにあえて非同期の処理も可能にしていたのですが、さすが halwhite さん、見逃しませんね笑
とてもスマートな回答です!

export default(b,t=document.createElement`canvas`.getContext`2d`)=>fetch(`test-cases/${t.drawImage(b,0,0),t.getImageData(8,32,1,1).data[0]?'kaya':'yap'}c.txt`).then(r=>r.text()




おまけ

ついでに『JS体操』マスコットキャラクターたちの紹介もしておきます!
お察しの通り、JavaScript で使用する演算子・記号類をキャラクター化したもの。跳び箱などの小物も実は作ってました。笑
3Dモデルは Blender で作成しています!




まとめ

さて、第1問〜第5問までを振り返ってみましたがいかがだったでしょうか?これで『JS体操』の施策は一旦区切りとなります。 問題を作るにあたって JavaScript の言語仕様を学び直したことで私自身とても勉強になりました。オライリーのサイ本第7版を読みながら、あ!これは問題にできそう、みたいなこともありました。業務でハマった経験を問題にすることもありました。そしてなにより挑戦者のみなさんが予想もしていなかったアプローチの回答で想定文字数を超えてくださるのがとても楽しく嬉しかったです。
最近では生の JavaScript を書く機会、細かいロジックを考える機会もなかなか減っているかもしれませんが、こういった形でコードゴルフという形で楽しむのも良いかもしれません。

『JS体操』は一旦終わりですが、社内勉強会用に作った問題は60問以上あるので、いつかまた再開するかもしれません。 なお、過去の問題に関してはいつでも挑戦可能ですので、まだやっていなかったという方は以下よりぜひ!

サポートいただいたたくさんの皆様ありがとうございました!
それではまた!!

hubspot.kayac.com

【Go】テーブル駆動テストのエラーチェックは関数パターンがおすすめ

記事公開時点ではSREの市川です。

というのも2024年の大晦日を以て退職となるのですが、実は【カヤック】面白法人グループ Advent Calendar 2024の7日目の記事をすっぽかしていたので、Go におけるテストの話を書いて置き土産といたします。

ケーススタディ

以下のようなSUT(テスト対象)があるとします。

package foo

func DoSomething(input string) int {
    // 何かしらの処理
}

この限りでは、SUTがエラーを返さないのでエラーチェックの必要はありません。つまり、以下のようなテストコードを書くことができます。

package foo_test

import (
    "testing"

    "foo" // your SUT package

    "github.com/stretchr/testify/require"
)

func TestDoSomething(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  int
    }{
        {
            name:  "when input is foo",
            input: "foo",
            want:  3,
        },
        // 他のテストケースを列挙
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := foo.DoSomething(tt.input)
            require.Equal(t, tt.want, got)
        })
    }
}

stretchr/testify についても、かなり普及しているモジュールだと思うので詳しい解説は割愛しますが、Go のテストにおけるアサーションを行うためのモジュールです。

require パッケージの諸関数は、要求を満たさなかった場合に当該テストを失敗としてゴルーチンを即時終了させます。 t.Run のコールバックは個別のゴルーチンで実行されるので、上記コードにおいて require が失敗時に中断する検証処理は個々のテストケースに閉じます。

エラーを返す関数のテスト

さて、本題に戻って、エラーを返す関数のテストをどう書くかを考えてみましょう。

先ほどの例を以下のように変更した場合を考えます。

package foo

func DoSomething(input string) (int, error) {
    // 何かしらの処理
}

この場合、テストコードはどう書けばよいでしょうか?

書き方① require.ErrorIs で比較

割とベーシックなのはこのパターンかなと思います。もちろんこれも下の例のようにSUTが返すエラーが変数として定義されていれば過不足なく検証可能です。

 func TestDoSomething(t *testing.T) {
        tests := []struct {
                name  string
                input string
                want  int
+               wantErr error
        }{
                {
                        name:  "when input is foo",
                        input: "foo",
                        want:  3,
                },
+               {
+                       name:    "invalid input",
+                       input:   "bar",
+                       wantErr: foo.ErrInvalidInput,
+               },
                // 他のテストケースを列挙
        }

        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       got := foo.DoSomething(tt.input)
-                       require.Equal(t, tt.want, got)
+                       got, err := foo.DoSomething(tt.input)
+                       require.ErrorIs(t, err, tt.wantErr)
+                       if err == nil {
+                               require.Equal(t, tt.want, got)
+                       }
                })
        }
 }

なお、wantErr が暗黙で nil になっている箇所もありますが、errors.Is(nil, nil)true になるので問題ありません。

stretchr/testify は内部的に Go の標準パッケージの errors を使っており、errors.Is() の挙動はPlaygroundで確かめることが可能です。

書き方② 関数で比較

これに対して、今回おすすめしたいのは、テストケースにエラーチェック用の関数を追加する方法です。

この方法のメリットは、とにかく柔軟にテストケースを記述できることです。エラーが単純な変数ではなく独自の型として定義されている場合の詳細な比較もできますし、「諸般の事情から文字列チェックをするしかない」みたいなケースにも簡単に対応できます。

 func TestDoSomething(t *testing.T) {
        tests := []struct {
                name  string
                input string
                want  int
+               errorCheck func(*testing.T, error)
        }{
                {
                        name:  "when input is foo",
                        input: "foo",
                        want:  3,
+                       errorCheck: func(t *testing.T, err error) {
+                               require.NoError(t, err)
+                       },
+               },
+               {
+                       name:  "invalid input",
+                       input: "bar",
+                       errorCheck: func(t *testing.T, err error) {
+                               require.ErrorIs(t, err, foo.ErrInvalidInput)
+                       },
+               },
+               {
+                       name:  "empty input",
+                       input: "",
+                       errorCheck: func(t *testing.T, err error) {
+                               if !strings.Contains(err.Error(), "empty input") {
+                                       t.Errorf("unexpected error message: %v", err)
+                               }
+                       },
                },
                // 他のテストケースを列挙
        }

        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       got := foo.DoSomething(tt.input)
-                       require.Equal(t, tt.want, got)
+                       got, err := foo.DoSomething(tt.input)
+                       tt.errorCheck(t, err)
+                       if err == nil {
+                               require.Equal(t, tt.want, got)
+                       }
                })
        }
 }

Go のテーブル駆動テストのコードは、とにかく縦長になる傾向があり、個々のテストケースと t.Run 内の検証処理を行ったり来たりすると疲れが溜まります。

そのため、できれば個々のテストケースだけを見た時に受ける直感を、完全に頼り切れるように設計したいと常々思っています。

ebi-yade/gotest/cases のご紹介

今どきLLMに入力補助をしてもらったり様々なソリューションがあるので「タイプ数が多くて面倒臭い」という気持ちとは折り合いをつけやすいですが、それもケースバイケースです。

好みによっては ebi-yade/gotest/cases というパッケージが助けになるかもしれません。

以下のように記述することで、エラーチェック関数の記述量を削減することが可能です。

package foo_test

import (
    // 略
    "github.com/ebi-yade/gotest/cases"
)

func TestDoSomething(t *testing.T) {
    tests := []struct {
        name       string
        input      string
        want       int
        errorCheck func(*testing.T, error)
    }{
        {
            name:       "when input is foo",
            input:      "foo",
            want:       3,
            errorCheck: cases.NoError,
        },
        {
            name:       "invalid input",
            input:      "bar",
            errorCheck: cases.ErrorIs(foo.ErrInvalidInput),
        },
    // 

ソースコードとしても t.Helper() を呼んで require をラップしている程度なので、もしインポートすることに抵抗があれば、プロジェクト内のユーティリティパッケージにコピペしていただいても構いません。

まとめ

テストは書くのも読むのも少なからず負担がかかりますが、テストの品質はソフトウェアの品質に大きな影響を与えます。 また、新たにチームに参加したエンジニアの幸福度にも直結する項目でもあると思います。

少しだけ厄介な問題に足を踏み入れることで、自信を持ってチームメンバーに仕事を任せられるようになると良いですね。

カヤックではテストで愛を表現できるエンジニアも募集しています❤️

hubspot.kayac.com









ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://techblog.kayac.com/#

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy