Nimで試しに宝くじ予想してみた

Hi!
こんにちは、英語も活字もダメなのにエンジニアしてる。
どうも僕です。

初投稿ですが
こういうのが苦手で変な日本語や
使っての解釈なので変な所があるかと思いますが
ご愛嬌という事で、許してやってください＠w＠
また、間違いや不明点に関してはどんどんツッコミをお願いします！
あなたの一言で僕はまた成長できますのでよろしくお願いします＞w＜


# はじめに
マークザッカーバーグの名言
`「Done is better than perfect.（完璧を目指すよりも、まずは終わらせろ。）」
`をもとに
自分で企画したモノを持ち寄って発表する部活『 つくったもん見せ部 』に参加しており
仕事で使ってる物より
**使ってみたい言語や技術を試そう**ということから
今回のサービスを作ってみました！
※ 今回の企画の為なので、運営し続けるかは不明ですww


## 何を作ったの？
Nimが使ってみたかったので
Nimで何か出来ないかなという事を考えてた時に
テレビで数字選択式の『 **Bingo5** 』を買って元が取れるかという企画をしていたので
選択範囲が限られてるので予想する位なら簡単かと思い作ってみました！

 **予測アルゴリズム**
今までの数字を集計した上で
テレビで紹介された**41の法則**と**プラス5の法則**を取り入れ
更に、それ以外に独自の法則を取り入れて
特定の数字の割合に修正した上で単純に**ランダム**で割り出す仕組みです

作ったものがこちら↓です。

https://bingo5.info/


## 環境について

**開発言語**
・Nim　*
・TypeScript　*

**フレームワーク**
・Jester　*
・Vue.js

**ライブラリ & モジュール**
・Vue Router
・Vuex
・vuex-module-decorators　*
・Vuetify.js　*
・axios
・その他諸々...

**その他環境**
・CentOS7
・Caddy　*
・SQLite3　*

\* がついてる物は今回、初めてないし本格的に使ったものになります。


# Nim
Nimを選んだ理由は
Goを勉強していた時に
調べていたら下記のようなサイトを見つけ**至高**や**イケてる**に引かれNimの勉強にきりかえました。

[至高の言語、Nimを始めるエンジニアへ - Qiita](https://qiita.com/rigani/items/6e87c7cee6903ed65ed2)
[イケてないのに人気がある golang vs イケてるのに人気がない Nim](http://wolfbash.hateblo.jp/entry/2017/10/02/163017)

説明は
どんな言語かは上記を読んでね！
わかりやすく説明されてるのでww

## 今回の使い方
まず、Webサービス化する前に
簡単に使ってみる為にコマンドとして開発。
その後
Webサービスとして開発が容易なフレームワークと出会い
Webサービス化した上でコマンドはそのまま利用にアレンジ。

※ データはいろんなデータが集計されている下記のサイトから収集。

[ビンゴ5(セブン)](https://tatuo.net/bingo5/)

## コマンド開発
開発したコンマンドは
・ データの登録
・ 予測発行
・ あたり判定
・ あたり集計
・ あたり集計の取得

どんな感じに書いたか
あたり判定のソースをペタリ。

```Nim
import
  db_sqlite,
  marshal,
  os,
  parseopt,
  strutils,
  tables

import
  ./private/helper,
  ./private/types/heldResult,
  ./private/types/target

type
  Output* = object
    position1*: int
    position2*: int
    position3*: int
    position4*: int
    position6*: int
    position7*: int
    position8*: int
    position9*: int
  Outputs* = object
    win1*: seq[int]
    win2*: seq[int]
    win3*: seq[int]
    win4*: seq[int]
    win5*: seq[int]
    win6*: seq[int]
    win7*: seq[int]
    target*: Output

template currentSourcePath: string = instantiationInfo(-1, true).filename.parentDir.parentDir
let dbPath: string = currentSourcePath & "SQLiteのファイルパス"
var args: seq[int]
var db: DbConn

for kind, key, value in getOpt():
  case kind
    of cmdArgument:
      args.add parseInt(key)
    else: discard

proc getResults(limit: int): seq[seq[string]] =
  var query = "SELECT held, position1, position2, position3, position4, position6, position7, position8, position9, ballset FROM result ORDER BY held DESC"
  if 0 < limit:
      query = query & " LIMIT " & intToStr(limit)

  return db.getAllRows(sql query)

proc convertTarget(args: seq[int]): Target =
  return Target(
    position1: args[0],
    position2: args[1],
    position3: args[2],
    position4: args[3],
    position6: args[4],
    position7: args[5],
    position8: args[6],
    position9: args[7])

proc check(target: Target, records: seq[seq[string]], db: DbConn): string =
  var heldResult: HeldResult
  var held: int
  var targetRecord: seq[int]
  var winning: int
  var output: Output = Output(
    position1: target.position1,
    position2: target.position2,
    position3: target.position3,
    position4: target.position4,
    position6: target.position6,
    position7: target.position7,
    position8: target.position8,
    position9: target.position9)
  var outputs: Outputs = Outputs(
    win1: @[],
    win2: @[],
    win3: @[],
    win4: @[],
    win5: @[],
    win6: @[],
    win7: @[],
    target: output)

  for record in records:
    held = parseInt record[0]
    targetRecord = @[parseInt(record[1]), parseInt(record[2]), parseInt(record[3]), parseInt(record[4]), parseInt(record[5]), parseInt(record[6]), parseInt(record[7]), parseInt(record[8]), parseInt(record[9])]
    heldResult = convertResult(held, targetRecord, db)
    winning = target.winning heldResult

    case winning:
      of 1:
        outputs.win1.add held

      of 2:
        outputs.win2.add held

      of 3:
        outputs.win3.add held

      of 4:
        outputs.win4.add held

      of 5:
        outputs.win5.add held

      of 6:
        outputs.win6.add held

      of 7:
        outputs.win7.add held

      else: discard

  return $$outputs

proc main(): void =
  block:
    db = open(dbPath, "ユーザ", "パスワード", "データベース")

    defer:
      db.close()

    let records = getResults args[8]
    let target = convertTarget args
    echo check(target, records, db)

when isMainModule:
  main()
```


## RESTサーバ開発
とりあえず有名なフレームワーク`Jester`を
Pythonでいう`pip`の様な、Nimのパッケージ管理`nimble`でインストールしてインポートしてポン！と、とても簡単！
`Jester`はSinatraライクなフレームワークで普段Sinatraを使ってる人は使いやすいのかもね！

RESTサーバのソースをペタリ。
今回は処理が少ない為( メイン処理はコマンドで記載済み )
RESTサーバのソースは、1枚のファイルにまとめてます。

```Nim
import
  asyncdispatch,
  asyncnet,
  jester,
  json,
  osproc,
  strutils,
  times

settings:
  port = Port(ポート)
  appName = "/api"

routes:
  get "/issues/@lots":
    let response = %*{"timestamp": $int(epochTime())}
    try:
      let lots: int = parseInt(@"lots")
      if lots <= 0 or 21 <= lots:
        raise newException(Exception, "Lots Unacceptable")
      else:
        response["lots"] = newJInt(lots)
        let issueCommand = "発行コマンドのパス " &  $response["lots"]
        let (output, exitCode) = execCmdEx(issueCommand)
        let outputJson = parseJson(output)
        response["results"] = outputJson["results"]
        resp $response, "application/json"
    except:
      let e = getCurrentException()
      let now: Time = getTime()
      let nowStr: string = format(now, "yyyy/MM/dd HH:mm")
      echo nowStr & " ----> " & $e.name & " : " & e.msg
      let traces = e.getStackTraceEntries()
      for trace in traces:
        echo $trace.filename & "(line: " & $trace.line & ")" & " - " & $trace.procname

      response["message"] = newJString("Bad Request - Parameter Unjust")
      resp Http400, $response, "application/json"

  post "/check":
    let response = %*{"timestamp": $int(epochTime())}
    try:
      let params = request.formData
      let target: int = parseInt(params["target"].body)
      if target != 0 and target != 1 and target != 10:
        raise newException(Exception, "Target Unacceptable")
      else:
        let checkCommandFromat: string = "チェックコマンドのパス $1 $2 $3 $4 $5 $6 $7 $8 $9"
        let jsons = parseJson(params["json"].body)
        if jsons.len <= 0 or 20 < jsons.len:
          raise newException(Exception, "JSON Unacceptable")
        else:
          var results: JsonNode = newJArray()
          var checkCommand: string
          for json in jsons:
            if target == 0:
              checkCommand = checkCommandFromat % [$json["col1"], $json["col2"], $json["col3"], $json["col4"], $json["col6"], $json["col7"], $json["col8"], $json["col9"], ""]
            else:
              checkCommand = checkCommandFromat % [$json["col1"], $json["col2"], $json["col3"], $json["col4"], $json["col6"], $json["col7"], $json["col8"], $json["col9"], $target]

            var (output, exitCode) = execCmdEx(checkCommand)
            var outputJson = parseJson(output)
            results.add outputJson

          response["results"] = results
          resp $response, "application/json"
    except:
      let e = getCurrentException()
      let now: Time = getTime()
      let nowStr: string = format(now, "yyyy/MM/dd HH:mm")
      echo nowStr & " ----> " & $e.name & " : " & e.msg
      let traces = e.getStackTraceEntries()
      for trace in traces:
        echo $trace.filename & "(line: " & $trace.line & ")" & " - " & $trace.procname

      response["message"] = newJString("Bad Request - Parameter Unjust")
      resp Http400, $response, "application/json"

  get "/winner":
    let response = %*{"timestamp": $int(epochTime())}
    let (output, exitCode) = execCmdEx("あたり集計の取得のパス")
    let outputJson = parseJson(output)
    response["results"] = outputJson
    resp $response, "application/json"

  error Http400:
    let response = %*{"message": "Bad Request", "timestamp": $int(epochTime())}
    resp Http400, $response, "application/json"

  error Http404:
    let response = %*{"message": "Not Found", "timestamp": $int(epochTime())}
    resp Http404, $response, "application/json"

  error Http500:
    let response = %*{"message": "Internal Server Error", "timestamp": $int(epochTime())}
    resp Http404, $response, "application/json"
```


## 感想
まだまだ、ドキュメントが少なく苦戦しまくった。。。
ただ、コンパイルした結果がバイナリーデータなので
本番公開はアップしてパーミッションを適正にするだけで出来たからデプロイは簡単だったー

また
GoやPythonの様な書き方でコードが少なくかけ
また、結構な省略ができるのでかなり楽!!
しかも、Javaと同じで引数違いの同じ関数名が書けるのは助かるー！＠w＠
でも
その反面、省略しすぎて少し可読性が下がってしまう時があったり
JavaScriptでも出来る記号が関数名に関してはかなり不明に陥る。。。
上のソースでも使われてる`$`や`$$`や`%*`で何が起きるかってドキュメントやソースをみないと・・・
まぁーチャント読めって話だけど (笑)

かなりハマったのは
`Error: type mismatch: got <型> but expected '型'`
という型のが間違ってるエラーで
基本、引数や省略して書いた結果代入時のミスで起こる現象だと思うが
僕が開発時に起きたのは
`result`という変数名を使うと
変数宣言してその時に正確に型宣言して関数を実行しても上記のエラーが出るという事があってかなり時間を使ってしまった。
型ミスの場合は一度変数名や関数名を疑うのもいいかもしれない・・・

# Vue.js + TypeScript
Vue.js + TypeScriptを選んだ理由は
Laravelを使った開発でVue.jsを使って使いやすかったので
他でも使ってみようと思いながら
なんとなく遠ざけてたTypeScriptをこの気なので選びました

## 今回の使い方
とりあえずデザインは考えるのは面倒いので
VueのUIコンポーネントライブラリの`Vuetify`でサクッとw
そして
やはり面倒なのでSPAにしてしまおうと
`Vue Router`を使ってまたまたサクッとw
状態管理を使う為に`Vuex`を利用してサクッとと言いたいのですが
TypeScriptで開発の為、`vuex-module-decorators`というライブラリを入れると便利と記事があったので右に倣えでインストール！
後は、Ajaxを使うのでAjax様に`axios`でやりとり

[vuex + typescriptをvuex-module-decoratorsで無敵になる - Qiita](https://qiita.com/tsrnk/items/fd95c3d8013d0795a260)

## 感想
ほとんど使った事あるものだったので苦労はほとんどなく`Vuetify`もあまり難しい事なかったんですがー
TypeScriptは初めてだったので、その部分で何個か苦戦した
Javaでいうアノテーションが使えるのは便利と感じた( TypeScriptだと`Decorator`っていうみたい )のと型宣言が安心＠w＠
今後はTypeScriptで書いていこうと思った！
ただ、アノテーションが使えるとは思ってなかったので`vuex-module-decorators`で混乱してしまった
その上で便利だけど呪文化しそうでそれがないと動かないとか何が起きてるのかが分からないってのが少しとりあえず書いとけってのが少し理解薄く今後の課題かなー

# Caddy
Goで作られたWebサーバーで選んだ理由は
面白そうの一点張りですが
面白そうな理由は一つは`QUIC`という新しいプロトコルと
簡単に`Let's Encrypt経由で無料証明書`を発行して設定してくれるみたいで楽そうだったから(てか、自動で発行設定してくれる)

## 設定ファイル

```
bingo5.info {
  root ルートパス
  proxy /api http://0.0.0.0:5555

  rewrite {
    if {path} not_starts_with /api
    regexp .*
    to {path} /
  }

  gzip
  log /var/log/caddy/access.log
  errors /var/log/caddy/error.log

  tls メールアドレス
}
```

最後の`tls`だけでhttpsが使えるなんて本当楽ー！！

## 感想
基本ソースからコンパイルしてーぽいんですが
環境がCentOS7だったので試しに確認してみたらでけたーなので超簡単でいた！

方法はー
ここに載ってたまま

[Install and Configure Caddy on CentOS 7](https://www.linode.com/docs/web-servers/caddy/install-and-configure-caddy-on-centos-7/)

**1. yumのアップデート**

```
# yum update
```

**2. EPELリポジトリ追加**

```
# yum install epel-release
```

**3. Caddyインストール**

```
# yum install caddy
```

で終わりました＠w＠

後は
ウェブのルートディレクトリを作って設定ファイルを適正に設定して
サービス登録してぽん！

注意点は
普通は`Caddyfile`という設定ファイルなのに対してyumでインストール
すると`/etc/caddy/caddy.conf`が大元設定で`/etc/caddy/conf.d/*.conf`が個別設定
それと
プラグインがどんだけインストールされてるか分からないけど(もしかして、一切入ってない？)
使いたかったCacheがインストールされてなかったので使えなかったww

# 総まとめ
複数の言語を同時開発はよくある事だと思うけど
新規で覚える時には正直一つずつをオススメしますねー
今回`Nim`と`TypeScript`を使う上で
型宣言で`int`と`number`とか`let`の動きの違いとかで混乱してしまったりしたので遊びでも分けてするべき(当たり前のことだけどww)

その上で
Nimは面白く今後も使っていきたいと思ったんですが
つくったもん見せ部でNim使っていくよーっていうと今回の企画をみた人にWeb言語ですか？
と言われ出来る事をうまく説明出来なかったので
今度はWeb以外で何か作るのとGoと比較されがちなのでGoで作ったCaddyのNim版もできたらなーと
それと
TypeScriptは積極的に使ってみようかな
コンパイルは面倒だけどVue.js使うなら結局一緒だし便利なのが盛りだくさんだから
で
Caddyもhttpsが必須な状況で
いちいち証明書設定など面倒なので簡単なサイトにはどんどん使ってみようかなと
あと
yumでインストールした時のプラグイン調査と
Goで作られてるので何かあれば便利ってのがプラグインとして作れるのであれば作ってみたいなー

後、初投稿で不慣れすぎて
かなり時間かかったー

それと
試したかっただけなので
本システムで2等や公開前のバージョンでは3等を当てましたが
あくまでルールに基づいてランダムで発行してるだけなので当選を保証するものではありません。
宝くじの購入は自己責任でお願いいたします。

---
**宣伝**
大阪で開催されてるのであれですが興味があれば見せ部に来てねー＠w＠
[作ったもん見せ部](https://misebu.connpass.com/)