JavaScript で「アウトプットのネタに困ったらこれ！」をやった

以下の10問を解ければ初心者脱出だよという記事があったので、Node.js (JavaScript) でやった。10問中5問（奇数番目だけ）やった。

添削してくれる人がいれば嬉しいです。一方、初心者ってほどひどいコードではないと思うので、Lodash とか最近のJSの書き方を知りたいっていう人には参考になるかもしれない。

- [アウトプットのネタに困ったらこれ！？Ruby初心者向けのプログラミング問題を集めてみた（全10問）](https://blog.jnito.com/entry/2019/05/03/121235)

# 00. 前提とお断り
- Node v12
- lodash は使う: `$npm i lodash` でインストール。`const _ = require('lodash')` でインポート。
- 問題文は私がサマライズしているので読み取りにくい場合は上記のURLに見に行った方がよい

# 01. カレンダー作成問題
問題:

```
次のような形式で今月のカレンダーを表示せよ
       April 2013
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
```

私の解き方:

- カレンダーをヘッダ部とコンテンツ部にわけて考える
- ヘッダ部
    - Aprilなど長い月名: ググって調べる
- コンテンツ部
  - 最初にスペースの塊があって そのあとに日付分だけ数字が続くとみなす
  - スペースの塊 = 見えない dummy な日付数 = 月初めの曜日のインデックス
  - パディング: 1桁も2桁も2文字に右詰めする
  - 何日ある？: 1日ずつ進めて 月が変わるまで処理=Dateのコンストラクタに日付を渡したときに、それが調整されたら月が変わったと判定する。
- 出力
  - コンテンツ部は dummy日付を含めて 7日ごとに改行。`_.chunk`が使える。

私の回答:

```js
const _ = require('lodash');

const now = new Date();
const MNAME = now.toLocaleString('en-us', { month: 'long' }); // long month name.
const YEAR = now.getFullYear();
const dummyDates = (new Date(YEAR, now.getMonth(), 1)).getDay();
const dates = _.takeWhile(_.range(1, 50),
  d => d === new Date(YEAR, now.getMonth(), d).getDate());

// output: console.log for every 7 days
console.log(`       ${MNAME} ${YEAR}\nSu Mo Tu We Th Fr Sa`);
_.chunk([...Array(dummyDates).fill(null), ...dates], 7).forEach(arr => {
  console.log(arr.map(n => n ? `${n}`.padStart(2) : `  `).join(' '));
});
```

ひとこと:

- 時差を考えてないので月末・月初は間違うかもしれない

# 02. カラオケマシン問題
偶数なのでパス

# 03. ビンゴカード作成問題

問題:

```
1から75までの数字をランダムに配置して、ビンゴカードを作成
 B |  I |  N |  G |  O
13 | 22 | 32 | 48 | 61
 3 | 23 | 43 | 53 | 63
 4 | 19 |    | 60 | 65
12 | 16 | 44 | 50 | 75
 2 | 28 | 33 | 56 | 68
```

私の解き方:

- データの作り方
    - 5要素のArrayを 5つ含むArrayで表現
    - 0..14 から5つの重複しない乱数を作成 `_.sampleSize`
    - 上記5つに オフセットを付ければデータの完成
- 表示の仕方
   - パディング: 問題1と同様
   - 真ん中の空白: インデックスで場合分け 

私の回答:

```js
const _ = require('lodash');

const DATA = _.range(5).map(i => _.sampleSize(_.range(15), 5).map(n => n + i * 15 + 1));
console.log(' B |  I |  N |  G |  O');
_.range(5).forEach(row => {
  const arr = DATA.map((a, col) => (row === 2 && col === 2) ? '  ' : `${a[row]}`.padStart(2));
  console.log(arr.join(' | '));
});
```

ひとこと:

- こんな規則があるとは知らなかった

# 04. ボーナスドリンク問題
偶数番目なのでパス

# 05. 電話帳作成問題

問題:

```
カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成
# INPUT
['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']

# OUTPUT 
[ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]
```

私の解き方

- `_.groupBy` を利用
    - 正規表現を使ってキー関数を構成（濁音もあるので正規表現が無難？）
- ソート
    - キーでソート
    - 電話帳でソート

私の回答

```js
const _ = require('lodash');

const keyfn = str => {
  const ch = str[0];
  if (ch.match(/[ア-オ]/)) return 'ア';
  if (ch.match(/[カ-コ]/)) return 'カ';
  if (ch.match(/[サ-ソ]/)) return 'サ';
  if (ch.match(/[タ-ト]/)) return 'タ';
  if (ch.match(/[ナ-ノ]/)) return 'ナ';
  if (ch.match(/[ハ-ホ]/)) return 'ハ';
  if (ch.match(/[マ-モ]/)) return 'マ';
  if (ch.match(/[ヤ-ヨ]/)) return 'ヤ';
  if (ch.match(/[ラ-ロ]/)) return 'ラ';
  if (ch.match(/[ワ-ン]/)) return 'ワ';
  return 'その他';
};

const main = (input) => {
  const grouped = _.toPairs(_.groupBy(input, keyfn));
  grouped.sort((g1, g2) => (g1[0] < g2[0]) ? -1 : 1);
  grouped.forEach(([, g]) => g.sort());
  return grouped;
};

const INPUT = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'];
console.log(main(INPUT));
```

ひとこと

- 日本語＋正規表現 をやったことないので自信がない


# 06. 国民の祝日.csv パースプログラム
偶数番目なのでパス

# 07. 「Rubyで英語記事に含まれてる英単語を数えて出現数順にソートする」問題

問題

```
# INPUT
Interior design and decorating resource Houzz is the overall best Android app of the year, （略）

# OUTPUT
単語数（熟語以外）：331
英熟語？------------------------------------------------------------------
  2 Google I/O
  2 Google Play Awards
  1 And Google
  1 Best App
  1 Best Game
  1 Best of
  1 Best Standout Startup
  1 Best Use of Google Play Game Services
  1 Best Use of Material Design
  (略)
英単語------------------------------------------------------------------
 22 the
 11 and
 11 of
  8 a
  6 apps
  5 app
  5 best
  5 for
```

準備: `$curl https://raw.githubusercontent.com/JunichiIto/extract-word-sandbox/master/test/input.txt > input.txt`

解き方:

- 先読みな正規表現あたりなのだろうけど、やり方がわからなかったので力任せに。
- Step1: 文章に分解(`,`と`.`で文章をsplit)
- Step2: 各文章において、以下を行うジェネレータを実装
   - Step2-1: 単語を `/[-'/\w]+/` で区切る
   - Step2-2: 単語ずつ読んで、`of`と大文字で始まる奴が連続していたらくっつけて返す
   - Step2-3: そうでなければ単語を返す
- Step3: 複合語と単語それぞれに `_.countBy` と ソートで整理

私の回答:

```js
const fs = require('fs');
const _ = require('lodash');

const isCompound = str => str === 'of' || str.match(/^[A-Z]/);

function * wordsOf (str) {
  const sentences = str.split(/[,.]/);
  for (const sentence of sentences) {
    const words = sentence.match(/[-'/\w]+/g);
    if (!words) continue;
    for (let buf = [], next; ;) {
      // (finish condition)
      if (words.length === 0) {
        if (buf.length !== 0) yield buf.join(' ');
        if (next !== null) yield next;
        break;
      }
      // (otherwise)
      next = words.splice(0, 1)[0];
      if (!isCompound(next)) {
        if (buf.length !== 0) yield [...buf].join(' ');
        yield next;
        buf = [];
        next = null;
        continue;
      }
      // if 'of' is the first occurence, no good.
      if (next === 'of' && buf.length === 0) {
        yield next;
        next = null;
        continue;
      }
      buf.push(next);
    }
  }
}

const main = str => {
  const words = [...wordsOf(str)];
  const cwords = words.filter(w => w.includes(' '));
  const swords = words.filter(w => !w.includes(' '));
  const cmp = (x, y) => (x[1] !== y[1]) ? x[1] - y[1] : x[0] < y[0] ? 1 : -1;
  const cresult = _.toPairs(_.countBy(cwords)).sort(cmp).reverse();
  cresult.forEach(([w, n]) => console.log(n, w));
  const sresult = _.toPairs(_.countBy(swords)).sort(cmp).reverse();
  sresult.forEach(([w, n]) => console.log(n, w));
};

main(fs.readFileSync('./input.txt', { encoding: 'utf8' }));

```

ひとこと

- ひどい。正規表現を勉強してあとで書きなおそう。


# 08. 行単位、列単位で合計値を求めるプログラム
偶数番目なのでパス

# 09. ガラケー文字入力問題
問題

```
"440330555055506660"を入力すると、"hello"が返ってきます。
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2006
```

私の解き方:

- トークナイズ: `0` で splitしてあげて 空文字列でないものが入力一覧となる
- 「トークンの1文字目」と「トークンの長さ」で何の文字かわかる
- トークンの長さで巡回するためには剰余計算を使うのが簡潔

私の回答:

```js
const DATA = [
  null,
  [...'.,!? '], [...'abc'], [...'def'],
  [...'ghi'], [...'jkl'], [...'mno'],
  [...'pqrs'], [...'tuv'], [...'wxyz'],
];

const fn = str => {
  const tokens = str.split('0').filter(x => x !== '');
  return tokens.map(token => {
    const [idx, num] = [+token[0], token.length];
    const d = DATA[idx];
    return d[(num + d.length - 1) % d.length];
  }).join('');
};

const TESTS = [
  ['a', '20'],
  ['b', '220'],
  ['b', '222220'],
  ['hello, world!', '44033055505550666011011111090666077705550301110'],
  ['keitai', '000555555550000330000444000080000200004440000'],
];

console.log(TESTS.every(([ans, x]) => fn(x) === ans));
```

ひとこと

- バリデーションを入れてもいいかも
- `0`を区切りとしない、という応用問題もありそう



# 10. 値札分割問題
偶数番目なのでパス