[JavaScript] JR新幹線の運賃計算がそれなりに複雑なのでテスト駆動で実装してみた

こちらの記事で知りました。

DDD くらいできるようになりたいよねって話 - BIGLOBE Style ｜ BIGLOBEの「はたらく人」と「トガッた技術」
https://style.biglobe.co.jp/entry/2020/01/15/130000

そこで、自分でも新幹線の運賃計算を行ってみました。
DDDくらいできなくても、このくらいの計算できるよね。という反骨精神です。

ちょっとしたプログラミングの課題として、練習のために自分で実装してみるのも面白いかも？です。

## 仕様

計算仕様はこちらにありました。

masuda220/jr-pricing
https://github.com/masuda220/jr-pricing#12月から１月の繁忙期と閑散期

> # JR 新幹線 料金ルールを実装してみよう
> 
> 今回はサンプルとして
> 
> - 出発地：東京
> - 目的地：新大阪または姫路
> 
> とする。
> 
> # 料金ルール (Fare System)
> 
> 料金 ＝ 運賃 + 特急料金
> 
> 運賃、特急料金は１０円単位が基本です。（券売機で１円硬貨、５円硬貨を扱わないため）
> 
> ## 基本の運賃と特急料金
> 
> 東京からの運賃と特急料金は以下の通り。
> 
> ### 運賃(basic fare)
> 
> - 新大阪まで 8,910 円
> - 姫路まで 10,010 円
> 
> ### 特急料金(super express surcharge)
> 
> #### 指定席(ひかり)
> 
> - 新大阪まで 5,490 円
> - 姫路まで 5,920 円
> 
> #### のぞみ割り増し (additional charge)
> 
> ひかりの特急料金に、以下の金額を加算する。
> 
> - 新大阪まで 320 円
> - 姫路まで 530 円
> 
> > 例: 新大阪までのひかり料金
> >
> > ```5,490円 + 320円 = 5,810円```
> 
> ### 自由席特急料金 (free seat)
> 
> 通常期の指定席(ひかり)特急料金から530円引きになります。
> 
> > 例：通常期の新大阪までの自由席特急券の料金
> >
> > ```5,490円 - 530円 = 4,960円```
> 
> ## 子供料金 (child)
> 
> 運賃、特急料金ともに、 こどもは半額
> 
> （5円の端数は運賃、特急料金それぞれで切り捨てます）
> 
> > 例： 新大阪までの子供料金
> >
> > ```運賃 ： 8,910円 × 50% = 4,455円 → 4,450円```
> >
> > ```特急料金ひかり : 5,490円 × 50% = 2,745円 → 2,740円```
> >
> > ```合計 : 7,190円```
>  
> 
> ## 割引 (discount)
> 
> ### 往復割引 (round trip discount)
> 
> 片道の営業キロが601km以上あれば、「ゆき」と「かえり」の運賃がそれぞれ1割引になります。
> 
> 東京からの営業キロ
> 
> - 新大阪 553km
> - 姫路 644km
> 
> > 例：東京-姫路の往復料金
> >
> > ```片道の割引額 10,010円 × 90% = 9,009円 → 10円未満の端数切り捨て 9,000円```
> >
> > ```割引後の往復運賃 ： 9,000円 × 2 = 18,000円```
> >
> > 2,020円の割引になる
> 
> ### 団体割引(group discount)
> 
> 8人以上が同一行程で旅行する場合に適用されます。
> 
> - 12月21日〜1月10日 10%
> - それ以外 15%
> 
> #### 31人以上の割引 
> 
> - 31〜50人の普通団体は、1人分の運賃と特急料金が無料になります
> - 51人以上の場合は、50人増えるごとに、1人ずつ運賃・特急料金が無料になります。
> 
> ## 季節(season)による特急指定席料金の変動
> 
> ### 季節の区分
> 
> - 通常期(regular)
> - 閑散期(off-peak)
> - 繁忙期(peak)
> 
> の三種類
> 
> ### 12月から１月の繁忙期と閑散期
> 
> - 繁忙期: 12月25日〜1月10日（年末年始)
> - 閑散期： 1月16日〜30日
> 
> ### 料金の変動
> 
> 通常期の指定席特急券に対して、
> 
> - 閑散期は -200円
> - 繁忙期には +200円
> 
> ### 自由席特急券
> 
> 季節によって変動しない。
> 通常期の指定席特急券より530円を引いた金額で年間固定。

DDDのBIGLOBE Styleの記事の方では追加の詳細仕様も書かれています。

> ## 料金計算について
> 仕様に明記されていない箇所は、以下のルールであるものとして実装しました。
> 
> - 往復割引のほか団体割引でも 10 円未満の端数は切り捨てる仕様としました。
> - 団体割引で 31 人以上の団体も 8 人以上の団体が受ける割引を受ける仕様としました。また大人と子供が混在している場合は、大人を優先して無料にします。
> 

自分の方もさらに細かい仕様を書いておきます。

- 往復割引(10%引き10円未満切り捨て)の後に、子供料金も半額10円未満切り捨てして、運賃を求めます。
- 8人以上の団体割引は、上記割引後の運賃と、特急料金に対して割引、これも10円未満切り捨てを行っています。

「全体を切り捨て無しで計算してから最後に10円未満切り捨て」するのか「毎回10円未満切り捨てして計算していく」するのか、どちらが現実にそって正しいのかどうかはしりませんが、たぶん後者のほうがよく使われるのではないかと思います。検算とかが便利そうだからです。

なのでその方式で手計算で求めた結果に対してプログラムが正しく動いているかをテストコードを書いて検証しています。

## コード

```js

const isBoolean = (value) => typeof value === 'boolean';

const round10Floor = (value) => {
  return Math.floor(value / 10) * 10;
}

const sum = (array) => {
  let result = 0;
  for (let i = 0, l = array.length; i < l; i += 1) {
    result += array[i];
  }
  return result;
};

const trainFareShinkansen = ({
  from, to, child,
  option,
}) => {
  if (from !== 'tokyo') { throw new Error(`trainFareShinkansen from:${from}`) }
  if (!['shinOsaka', 'himeji'].includes(to)) { throw new Error(`trainFareShinkansen to:${to}`) }

  if (!isBoolean(child)) {
    throw new Error(`trainFareShinkansen child:${child}`)
  }
  if (!isBoolean(option.nozomi)) {
    throw new Error(`trainFareShinkansen option.nozomi:${option.nozomi}`)
  }
  if (!isBoolean(option.unreservedSeat)) {
    throw new Error(`trainFareShinkansen option.unreservedSeat:${option.unreservedSeat}`)
  }
  if (!['peak', 'offpeak', 'regular'].includes(option.season)) {
    throw new Error(`trainFareShinkansen option.season:${option.season}`)
  }
  if (!isBoolean(option.roundTrip)) {
    throw new Error(`trainFareShinkansen option.roundTrip:${option.roundTrip}`)
  }

  const trainFareBasic = (from, to, option) => {
    let result = 0;
    if (to === 'shinOsaka') {
      result = 8910;
    } else if (to === 'himeji') {
      result = 10010;
      if (option.roundTrip)
      result = round10Floor( result * 0.9 );
    }
    return result;
  }

  const trainFareSuperExpressSurcharge = (from, to, option) => {

    let result = 0;
    if (to === 'shinOsaka') {
      result = 5490;
    } else if (to === 'himeji') {
      result = 5920;
    }

    if (option.nozomi) {
      if (to === 'shinOsaka') {
        result += 320;
      } else if (to === 'himeji') {
        result += 530;
      }
    }
    if (option.unreservedSeat) {
      result -= 530;
    } else if (option.season === 'peak') {
      result += 200;
    } else if (option.season === 'offpeak') {
      result -= 200;
    }

    return result;
  }

  let basicFate = trainFareBasic(from, to, option);
  let superExpress = trainFareSuperExpressSurcharge(from, to, option);
  if (child) {
    basicFate = round10Floor( basicFate / 2 );
    superExpress = round10Floor( superExpress / 2 );
  }

  if (option.roundTrip) {
    return {
      from, to, option,
      basicFate, superExpress,
      total: (basicFate + superExpress) * 2,
    };
  } else {
    return {
      from, to, option,
      basicFate, superExpress,
      total: basicFate + superExpress,
    };
  }

}

// shinOsaka
(() => {
  let param = {
    from: 'tokyo', to: 'shinOsaka',
    child: false,
    option: {
      nozomi: false,
      unreservedSeat: false,
      season: 'regular',
      roundTrip: false,
    }
  }
  let result;
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 8910, `${result.basicFate}`);
  console.assert(result.superExpress === 5490, `${result.superExpress}`);
  console.assert(result.total === 8910 + 5490, `${result.total}`);

  param.option.nozomi = true;
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 8910, `${result.basicFate}`);
  console.assert(result.superExpress === 5490 + 320, `${result.superExpress}`);

  param.option.nozomi = false;
  param.option.unreservedSeat = true;
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 8910, `${result.basicFate}`);
  console.assert(result.superExpress === 5490 - 530, `${result.superExpress}`);

  param.option.nozomi = true;
  param.option.unreservedSeat = true;
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 8910, `${result.basicFate}`);
  console.assert(result.superExpress === 5490 + 320 - 530, `${result.superExpress}`);

  param.option.nozomi = false;
  param.option.unreservedSeat = false;
  param.child = true
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 4450, `${result.basicFate}`);
  console.assert(result.superExpress === 2740, `${result.superExpress}`);

  param.child = false
  param.option.season = 'peak'
  {
    param.option.nozomi = false;
    param.option.unreservedSeat = false;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 + 200, `${result.superExpress}`);

    param.option.nozomi = true;
    param.option.unreservedSeat = false;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 + 320 + 200, `${result.superExpress}`);

    param.option.nozomi = false;
    param.option.unreservedSeat = true;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 - 530, `${result.superExpress}`);

    param.option.nozomi = true;
    param.option.unreservedSeat = true;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 + 320 - 530, `${result.superExpress}`);
  }

  param.option.season = 'offpeak'
  {
    param.option.nozomi = false;
    param.option.unreservedSeat = false;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 - 200, `${result.superExpress}`);

    param.option.nozomi = true;
    param.option.unreservedSeat = false;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 + 320 - 200, `${result.superExpress}`);

    param.option.nozomi = false;
    param.option.unreservedSeat = true;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 - 530, `${result.superExpress}`);

    param.option.nozomi = true;
    param.option.unreservedSeat = true;
    result = trainFareShinkansen(param);
    console.assert(result.basicFate === 8910, `${result.basicFate}`);
    console.assert(result.superExpress === 5490 + 320 - 530, `${result.superExpress}`);
  }

  param = {
    from: 'tokyo', to: 'shinOsaka',
    child: false,
    option: {
      nozomi: false,
      unreservedSeat: false,
      season: 'regular',
      roundTrip: true,
    }
  }
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 8910, `${result.basicFate}`);
  console.assert(result.superExpress === 5490, `${result.superExpress}`);
  console.assert(result.total === (8910 + 5490) * 2, `${result.total}`);

})();

// himeji
(() => {
  let param;
  param = {
    from: 'tokyo', to: 'himeji',
    child: false,
    option: {
      nozomi: false,
      unreservedSeat: false,
      season: 'regular',
      roundTrip: false,
    }
  }
  let result;
  result = trainFareShinkansen(param)
  console.assert(result.basicFate === 10010, `${result.basicFate}`);
  console.assert(result.superExpress === 5920, `${result.superExpress}`);

  param.option.nozomi = true;
  result = trainFareShinkansen(param);
  console.assert(result.superExpress === 5920 + 530, `${result.superExpress}`);

  param.option.nozomi = false;
  param.option.unreservedSeat = true;
  result = trainFareShinkansen(param);
  console.assert(result.superExpress === 5920 - 530, `${result.superExpress}`);

  param.option.nozomi = true;
  param.option.unreservedSeat = true;
  result = trainFareShinkansen(param);
  console.assert(result.superExpress === 5920 + 530 - 530, `${result.superExpress}`);

  param = {
    from: 'tokyo', to: 'himeji',
    child: false,
    option: {
      nozomi: false,
      unreservedSeat: false,
      season: 'regular',
      roundTrip: true,
    }
  }
  result = trainFareShinkansen(param);
  console.assert(result.basicFate === 9000, `${result.basicFate}`);
  console.assert(result.superExpress === 5920, `${result.superExpress}`);
  console.assert(result.total === (10010 + 5920) * 2 - 2020, `${result.total}`);

})();

const trainFareShinkansenMembers = ({
  from, to, option,
  members,
  groupDiscount,
}) => {
  if (!Number.isSafeInteger(members.adult)) {
    throw new Error(`trainFareShinkansenMembers members.adult:${members.adult}`)
  }
  if (!Number.isSafeInteger(members.child)) {
    throw new Error(`trainFareShinkansenMembers members.child:${members.child}`)
  }
  if (!isBoolean(groupDiscount.more8NewYear)) {
    throw new Error(`trainFareShinkansenMembers groupDiscount.more8NewYear:${groupDiscount.more8NewYear}`)
  }

  const mc = members.adult + members.child;
  let discountFuncMore8;
  if (8 <= mc) {
    if (groupDiscount.more8NewYear) {
      discountFuncMore8 = value => round10Floor( value * 0.9 );
    } else {
      discountFuncMore8 = value => round10Floor( value * 0.85 );
    }
  } else {
    discountFuncMore8 = value => value;
  }

  let discountMemberCount = 0;
  if (31 <= mc && mc <= 50) {
    discountMemberCount = 1;
  } else if (51 <= mc && mc <= 100) {
    discountMemberCount = 2;
  } else if (101 <= mc && mc <= 150) {
    discountMemberCount = 3;
  } else if (151 <= mc && mc <= 200) {
    discountMemberCount = 4;
  } else if (201 <= mc && mc <= 250) {
    discountMemberCount = 5;
  } else if (251 <= mc && mc <= 300) {
    discountMemberCount = 6;
  }

  const resultArray = [];
  for (let i = 1, l = members.child; i <= l; i += 1) {
    resultArray.push(
      trainFareShinkansen({from, to, child: true, option})
    );
  }
  for (let i = 1, l = members.adult; i <= l; i += 1) {
    resultArray.push(
      trainFareShinkansen({from, to, child: false, option})
    );
  }

  const total = sum(resultArray.map(value => value.total));

  for (let i = 1, l = discountMemberCount; i <= l; i += 1) {
    resultArray.pop();
  }

  const discountTotal = sum(
    resultArray.map(value => discountFuncMore8(value.total))
  )

  return {
    total,
    discountTotal,
  }
}

(() => {
  let option = {
    nozomi: false,
    unreservedSeat: false,
    season: 'regular',
    roundTrip: false,
  };
  let param = {
    from: 'tokyo', to: 'shinOsaka',
    option,
    members: { adult: 1, child: 0 },
    groupDiscount: { more8NewYear: false }
  };
  let result;

  // adult
  {
    result = trainFareShinkansenMembers(param);
    // console.log(result)
    console.assert(result.total === 8910 + 5490, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    param.members = { adult: 2, child: 0 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (8910 + 5490) * 2, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    param.members = { adult: 7, child: 0 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (8910 + 5490) * 7, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    {
      param.members = { adult: 8, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 8, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 8, `${result.total}`, `${result.discountTotal}`)

      param.groupDiscount.more8NewYear = true
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 8, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.9) * 8, `${result.total}`, `${result.discountTotal}`)

      param.groupDiscount.more8NewYear = false
    }

    {
      param.members = { adult: 30, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 30, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 30, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 31, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 31, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 30, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 50, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 50, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 49, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 51, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 51, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 49, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 100, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 100, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 98, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 101, child: 0 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 101, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((8910 + 5490) * 0.85) * 98, `${result.total}`, `${result.discountTotal}`)
    }
  }

  // child
  {
    param.members = { adult: 0, child: 1 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === 4450 + 2740, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    param.members = { adult: 0, child: 2 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (4450 + 2740) * 2, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    param.members = { adult: 0, child: 7 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (4450 + 2740) * 7, `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    {
      param.members = { adult: 0, child: 8 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 8, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 8, `${result.total}`, `${result.discountTotal}`)

      param.groupDiscount.more8NewYear = true
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 8, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.9) * 8, `${result.total}`, `${result.discountTotal}`)
      param.groupDiscount.more8NewYear = false
    }

    {
      param.members = { adult: 0, child: 30 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 30, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 30, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 0, child: 31 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 31, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 30, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 0, child: 50 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 50, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 49, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 0, child: 51 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 51, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 49, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 0, child: 100 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 100, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 98, `${result.total}`, `${result.discountTotal}`)

      param.members = { adult: 0, child: 101 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (4450 + 2740) * 101, `${result.total}`)
      console.assert(result.discountTotal === round10Floor((4450 + 2740) * 0.85) * 98, `${result.total}`, `${result.discountTotal}`)
    }
  }

  // adult child
  {
    param.members = { adult: 2, child: 2 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (8910 + 5490) * 2 + (4450 + 2740) * 2 , `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    param.members = { adult: 4, child: 3 }
    result = trainFareShinkansenMembers(param);
    console.assert(result.total === (8910 + 5490) * 4 + (4450 + 2740) * 3 , `${result.total}`)
    console.assert(result.total === result.discountTotal, `${result.discountTotal}`)

    {
      param.members = { adult: 4, child: 4 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 4 + (4450 + 2740) * 4 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 4 + round10Floor((4450 + 2740) * 0.85) * 4,
        `${result.total}`, `${result.discountTotal}`
      )

      param.groupDiscount.more8NewYear = true
      param.members = { adult: 4, child: 4 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 4 + (4450 + 2740) * 4 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.9) * 4 + round10Floor((4450 + 2740) * 0.9) * 4,
        `${result.total}`, `${result.discountTotal}`
      )
      param.groupDiscount.more8NewYear = false
    }

    {
      param.members = { adult: 1, child: 29 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 1 + (4450 + 2740) * 29 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 1 + round10Floor((4450 + 2740) * 0.85) * 29,
        `${result.total}`, `${result.discountTotal}`
      )

      param.members = { adult: 1, child: 30 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 1 + (4450 + 2740) * 30 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 0 + round10Floor((4450 + 2740) * 0.85) * 30,
        `${result.total}`, `${result.discountTotal}`
      )

      param.members = { adult: 1, child: 49 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 1 + (4450 + 2740) * 49 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 0 + round10Floor((4450 + 2740) * 0.85) * 49,
        `${result.total}`, `${result.discountTotal}`
      )

      param.members = { adult: 1, child: 50 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 1 + (4450 + 2740) * 50 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 0 + round10Floor((4450 + 2740) * 0.85) * 49,
        `${result.total}`, `${result.discountTotal}`
      )

      param.members = { adult: 2, child: 49 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 2 + (4450 + 2740) * 49 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 0 + round10Floor((4450 + 2740) * 0.85) * 49,
        `${result.total}`, `${result.discountTotal}`
      )

      param.members = { adult: 3, child: 48 }
      result = trainFareShinkansenMembers(param);
      console.assert(result.total === (8910 + 5490) * 3 + (4450 + 2740) * 48 , `${result.total}`)
      console.assert(result.discountTotal ===
        round10Floor((8910 + 5490) * 0.85) * 1 + round10Floor((4450 + 2740) * 0.85) * 48,
        `${result.total}`, `${result.discountTotal}`
      )

    }

  }

})();

```

## 解説

isBoolean / round10Floor / sum は、それぞれ共通関数です。

trainFareShinkansen は、一人用の交通費計算です。
引数として、from to と、childフラグと
option として、
  のぞみかどうか、自由席かどうか、季節は？繁忙期,閑散期,通常期、
  往復かどうか、
を指定しています。

戻る値は、
運賃、特急料金、トータル
です。

のぞみと、自由席、と、季節、で特急料金が変わり、往復なら、運賃が10%引き10円切り捨て

その計算後に、子供なら運賃/特急料金ともに半額、10円切り捨て、

で、往復なら倍額、としています。最後にまとめて10円切り捨て、ではないのが現実的かと思ってこうしています。

往復をここに盛り込むのは割引運賃を計算するためです。

返す値は
  運賃(片道)、特急料金(片道)、合計
としています。
割引総額とかも返してもいいかも。

trainFareShinkansenMembers は、複数人用の交通費計算です。
引数として
  from to, option (のぞみ、自由席、季節、往復)
  メンバー(大人x人、子供x人)
  グループ割引の季節(新年付近かどうか)
をわたしています。

面倒だったので、300人まで、の計算してみました。団体で300人を超える、ってのは、あるのかないのかしりませんが必要ならコードを数行増やすのは簡単。

仕様にあうように、子供の金額と、大人の金額を、8人以上の場合の割引された額で配列にいれて、割引対象の人数を大人の方から消しています。

子供が50人で、大人が1人の場合、51人以上は2名無料なので、大人1人、子供1人が無料となります。

## 実装の感想

実装してみて思うのは、元の仕様の書き方がちょっとな、というか、仕様把握が大切です。

> 季節(season)による特急指定席料金の変動
> 往復割引 (round trip discount)

ここがわかりにくかったなと思うのですが、これらは、それぞれ特急料金と運賃にかかる、ということを見抜かないといけないんだなということ。
これは人数が関係ないから、一人の運賃計算のときにも考慮する必要があります。

団体割引は団体のときになので、一人の時の計算方法とは異なって実装すべきだな、とか、そういう一工夫がいりました。

最初はわからなかったので、ひとり運賃計算のあと、団体計算時に季節変動や往復も組み込もうとしたらなんか変だったので、実装を修正したりです。

これも最初はわからずでしたが、団体で無料人数がでると、ひとりあたりいくらになるか、のひとりあたりの割引額を求めようとしましたが、大人と子供で金額がもともと違うし、無料枠が大人か子供がどちらがわりあたるかわからないので、そういう計算方法はやめておきました。

## テスト駆動おすすめ

厳密にテストを書いてからコードを書いた、というわけでもないのですが、実装しながらテストも書いていきました。

テストがあるおかげでコードに誤動作がないかどうかの心配がなく安心してコードかけました。そうでもしないとこの課題の計算は結果が非常に複雑なので、テストがないと安心感がないので、なかなか苦労します。

テストで仕様の通りに計算式をだして実行確認しているので、おそらく、仕様の漏れなんかはないと思うのですが、そもそものテスト実装ミスや仕様の理解ミスがあったら(やさしく)ご指摘ください。

実装は自分で考えて、上記のテストコードだけは利用するという方法でも練習になるかもです。

以上です。