概要
コラボキャンペーンが面白そうなので、久しぶりにPaizaの問題を解いてみました。
せっかくなので、ローカルの環境でもテストができる環境構築も整え、解説してみようかと思います。
少し前まで、Paizaのエディターで直接書いて提出していました。もちろんテストなしでした(笑)
提出コードは Go言語を使用しています。(私の一番得意な言語なので)
想定する読者
- 就職活動でPaizaを利用している人
- エンジニアになってまだ経験が浅い人
この記事の目的
- Paizaのスキルチェックを受け、エンジニアへの就職(転職)を成功させる
この記事で解説すること
- 実務のコードと競技プログラミングのコードの考え方
- Paizaの問題をローカル環境で書いて試す
ローカルでの開発環境を整えることで、より効果的、効率的にPaizaのスキルチェックを受ける。
ただスキルチェックを受けるだけでなく、企業の目に止まりやすいコーディングができるようになる。
これらを目指して、就職(転職)を成功させてほしいと思います。
この記事では解説していないこと
提出コードの具体的な解説。
この記事では、解答コードの中身よりも、問題を解く環境構築および、実務のコーディングと競技プログラミングでの意識する天の違いを解説することに重きをおいています。
そのため、提出コードの中身やGo言語の仕組み、テストの考え方については触れていません。
とはいえ、質問には応えようと思いますので、お気軽にコメントしてください。
需要があれば、提出コードの解説や詳しい説明などを執筆しようと思います。
コメントをお待ちしております。
問題文
今年もパイザ宝くじの季節がやってきました。パイザ宝くじ券には 100000 以上 199999 以下の 6 桁の番号がついています。毎年1つ当選番号 (100000 以上 199999 以下)が発表され、当たりクジの番号が以下のように決まります。
1等:当選番号と一致する番号
前後賞:当選番号の ±1 の番号(当選番号が 100000 または 199999 の場合,前後賞は一つしかありません)
2等:当選番号と下 4 桁が一致する番号(1等に該当する番号を除く)
3等:当選番号と下 3 桁が一致する番号(1等および2等に該当する番号を除く)たとえば、当選番号が 142358 の場合、当たりの番号は以下のようになります。
1等:142358
前後賞:142357 と 142359
2等:102358, 112358, 122358, …, 192358 (全 9 個)
3等:100358, 101358, 102358, …, 199358 (全 90 個)あなたが購入した n 枚の宝くじ券の各番号が入力されるので、それぞれの番号について、何等に当選したかを出力するプログラムを書いて下さい。
提出コード
package main
import (
"fmt"
"io"
"os"
"strconv"
"strings"
)
var (
InputSteam io.Reader = os.Stdin
OutputStream io.Writer = os.Stdout
InputLineBreak = "\n"
)
type Ticket string
const (
Output_First = "first"
Output_Adjacent = "adjacent"
Output_Second = "second"
Output_Third = "third"
Output_Blank = "blank"
)
const (
TicketNumber_Min = 100000
TicketNumber_Max = 199999
)
func NewTicket(s string) Ticket {
return Ticket(s)
}
func (t Ticket) Judge(winning string) string {
if string(t) == winning {
return Output_First
}
{
ticket, _ := strconv.Atoi(string(t))
winning, _ := strconv.Atoi(winning)
if winning >= TicketNumber_Min && ticket-1 == winning {
return Output_Adjacent
}
if winning <= TicketNumber_Max && ticket+1 == winning {
return Output_Adjacent
}
}
{
ticket := string([]rune(string(t))[2:])
if ticket == string([]rune(winning)[2:]) {
return Output_Second
}
ticket = string([]rune(string(t))[3:])
if ticket == string([]rune(winning)[3:]) {
return Output_Third
}
}
return Output_Blank
}
func main() {
input, _ := io.ReadAll(InputSteam)
values := strings.Split(normalizeInput(input), InputLineBreak)
winning := values[0]
for _, v := range values[2:] {
ticket := NewTicket(v)
fmt.Fprintln(OutputStream, ticket.Judge(winning))
}
}
func normalizeInput(input []byte) string {
return strings.TrimRight(string(input), InputLineBreak)
}
解説
競技プログラミングにしては長いコード
私の提出したハードコーディングをせず、宣言した定数を返すようになっていたりと、 競技プログラミングにおいては 冗長な書き方になってます。
あえて冗長な書き方になっているのは、企業の目に止まりやすいようにするためです。
短いコードがかけるよりも、可読性に優れたコードを書ける人材のほうが、実務では有利です。
そのためにあえて、冗長に書いています。
競技プログラミングの種類にもよるとは思いますが、基本的に解答コードは短く書きます。
理由は、解答コードが短いほど、スコアが高いことに由来します。
競技プログラミングの書き方でないもの(例)
- 定数宣言ではなく、ハードコーディングする
- 標準出力はfmt.Fprintlnではなくfmt.Printlnを使う
- 関数normalizeInputを宣言しているが、処理を直接書く
定数宣言ではなく、ハードコーディングする
...
- const (
- Output_First = "first"
...
func (t Ticket) Judge(winning string) string {
if string(t) == winning {
- return Output_First
+ return "first"
...
標準出力はfmt.Fprintlnではなくfmt.Printlnを使う
- var (
- InputSteam io.Reader = os.Stdin
- OutputStream io.Writer = os.Stdout
...
func main(){
...
- fmt.Fprintln(OutputStream, ticket.Judge(winning))
+ fmt.Println(ticket.Judge(winning))
他にもvalue[0]をwinningに代入をせず、直接使うようにする。
変数、関数名を短いものするなどができそうですね。
逆に、
実務のプログラミングでは気をつけるもの
- エラーハンドリングを書く
- シャドーイングを(極力)しない
関数normalizeInputを宣言しているが、処理を直接書く
func main() {
input, _ := io.ReadAll(InputSteam)
- values := strings.Split(normalizeInput(input), InputLineBreak)
+ values := strings.Split(strings.TrimRight(string(input), InputLineBreak)
, InputLineBreak)
...
-func normalizeInput(input []byte) string {
- return strings.TrimRight(string(input), -InputLineBreak)
}
normalizeInputは一箇所でしか使っていないので、直接書くことでコードを短くできますね。
当然ですが、可読性と保守性は下がります。
### シャドーイングを(極力)しない
シャドーイングとは、異なるスコープにある変数の名前を使い、別の変数に割り当てるような処理のことをいいます。(説明が下手ですね...)
同じ名前で異なる意味の変数が存在することになるため、使い所を間違えると可読性が落ちます。
シャドーイングについての説明は、以下の記事が参考になりました。
詳しくは、こちらをご覧ください。
その他にも、OSの違いや入力のバリデーションチェックなどが、実務のコードでは必要ですね。
競技プログラミングでは、省略されることが一般的ですが。
このコードにおけるOSの違いというのは、改行コードですね。
Windowsの場合キャリッジ・リターンというものを使用しています。
そのため、"\n"での分割が、正しく動かない可能性があります。
バリデーションというのは、文字列を数字への変換や、宝くじの番号の制約から外れたものの判定などです。
競技プログラミングでは省略することが一般的ですが、意識することでプログラミングへの理解が深まるかと思います。
テストについて
入力と出力に着目してテストを行うブラックボックステストの構造になっています。
提出コードをテストする際、変更を加えることなく、テストができるようになっています。
package main
import (
"bytes"
"io"
"strings"
"testing"
)
func Test(t *testing.T) {
values := []string{
"142350",
"5",
// first
"142350",
// adjacent
"142349",
// second
"992350",
// third
"999350",
// blank
"999999",
}
reader := strings.NewReader(strings.Join(values, InputLineBreak))
InputSteam = reader
outputBuffer := bytes.NewBuffer(nil)
OutputStream = outputBuffer
correct := []string{
Output_First,
Output_Adjacent,
Output_Second,
Output_Third,
Output_Blank,
}
main()
input, _ := io.ReadAll(outputBuffer)
outputValues := strings.Split(normalizeInput(input), "\n")
if len(correct) != len(outputValues) {
t.Fatalf("invalid len(correct)=> out:%d, correct:%d", len(outputValues), len(correct))
}
for i, v := range outputValues {
if v != correct[i] {
t.Errorf("invalid output=> testcase:%s, out:%s, correct:%s", values[i+2], v, correct[i])
}
}
}
出力と入力を偽装する仕組み
該当する部分のコード
...
var (
InputSteam io.Reader = os.Stdin
OutputStream io.Writer = os.Stdout
InputLineBreak = "\n"
)
...
...
reader := strings.NewReader(strings.Join(values, InputLineBreak))
// main.goにあるInputStreamを置き換えている
InputSteam = reader
outputBuffer := bytes.NewBuffer(nil)
// main.goにあるOutputStreamを置き換えている
OutputStream = outputBuffer
...
標準入力から入力を受け取り、標準出力に出力するのが基本です。
普段出力する時は fmt.Println("文字列")
のようにしていると思います。
このままでも、提出コードとしては問題ないのですが、ローカルの環境でテストするのには不都合です。
OSの仕組みを利用することも可能ですが、やや大掛かりな気がします。
そこで、テストのときはOSの標準入出力の代わりにそれぞれの代わりの入出力(ストリーム)をセットし、入出力を偽装することを思いつきました。
入力のストリームに値をセットしておくことで、あたかも標準入力で入力されたように見せかけています。
出力のストリームも同様に、書き込まれた値を読み込むことで、テスト側のコードで出力を検証できるようにしています。
ディレクトリ構造
自分の場合、「paiza」というディレクトリを作り、サブディレクトリに問題のコードとテストコードを書きました
paiza
├ /問題A
│ ├ main.go //提出するコード
| └ main_test.go //テストコード
…
├ /問題Z
└ go.mod
go modulesの利用 を前提とした構造になってます。
まとめ
goでPaizaの問題を解く時は、使いまわして書く時間を減らせるようにすることで、解答時間の短縮を図りました。
また、スキルチェックで提出したコードを 企業の担当者が見て読みやすいコードであるか の視点で書きました。
これは、私が学生時代のときの経験談ですが、競技プログラミングで優れたコードをかける人材 = 企業が欲しい人材とは限らないと感じたからです。
今は人のコードをレビューする立場で、採用にも関わっていますが、やはりこの考えは変わってないですね。
人から見て読みやすいコードを書く というのは、実務では当たり前のスキルとなりますので、今から磨いておくことがおすすめです。
実際に採用面接を受けるときも、「可読性を意識し、あえて競技プログラミングとしては長いコードを提出しました」と、アピールしてもらえば、(少なくとも私は)好印象です。
最後までご覧いただき、ありがとうございます。
誤字脱字や、内容に不正確なものが含まれているなど、何かありましたらコメントにて教えていただけると幸いでございます。
余談
宝くじの件をTicketとしているのは、夢を叶える チケット というシャレみたいなものです。
分かりやすいネーミングは大事です。
注意
スキルチェックの問題文および解答コードを共有することは、利用規約で禁止されています。
Paizaを使い始めたばかりの人は、お気をつけください。
また、AIを使って提出するコードを作成することも、利用規約で禁止されています。
AIを活用できるのは、これからの時代で必要ではありますが、Paizaのスキルチェックはあくまで自分のコーディング力で、プログラミングしてください。