タイムズカープラスで最安プランを選ぶ

# はじめに
カーシェアリングサービス「[タイムズカープラス](https://plus.timescar.jp/)」のプラン料金を計算し，最安プランを選びます．

## 予約作業について
[タイムズカープラス](https://plus.timescar.jp/)で車を予約をする際には，WEBページ上で次のような作業をする必要があります．

1. 借りたい車を選択する
1. 料金プランを選択する
1. 利用開始時刻と返却予定時刻を入力する
1. 料金を確認して問題なければ予約を確定する

ここで「料金プラン」には選択肢が幾つかあるのですが，それぞれの料金を確認するには画面遷移が発生します．また，距離に応じた料金がかかるプランとかからないプランがあるのですが，想定走行距離は入力できないため，確認する際の料金には含まれていないことになります．

ということで，想定走行距離を含めた全プランの料金を計算し，最も小さな金額を自動的に選ぶ，というコードを書いてみます．

## 料金プランについて
タイムズカープラスで，「ベーシック」と呼ばれる車種を借りる場合の[利用料金](https://plus.timescar.jp/fare/use.html)は，次の表のようになっています．

| プラン名       |    料金| 距離料金 | 延長料金 |
|:-------------|--------:|--------------:|----------------:|
|ショート         |        0円|         0円/km|      206円/15分|
| 6時間パック    |     4,020円|         0円/km|      206円/15分|
|12時間パック    |     6,690円|        16円/km|      206円/15分|
|24時間パック    |     8,230円|        16円/km|      206円/15分|
|アーリーナイトパック 18-0時|     2,060円|        16円/km|      206円/15分|
|レイトナイトパック 0-9時|     2,060円|        16円/km|      206円/15分|
|ダブルナイトパック 18-9時|     2,580円|        16円/km|      206円/15分|

「ショート」のプランは，「延長料金が206円/15分の0時間(0円)パック」と言う風に捉えると，他の「○時間パック」と同じように計算できます．

なお，複数のプランを組み合わせることはできません[^1]．また，延長時間には[上限](https://plus.timescar.jp/faq/use/reserve/17.html)があり，ショートの場合は72時間まで，パックの場合は6時間までです．

（別途，初期費用や基本料金がかかる場合があります．また，36時間パック，48時間パック，60時間パックというプランが選択可能な車両もあります．詳細は[公式サイト](https://plus.timescar.jp/)を参照してください）

# Pythonコード

## 各プランの定義

まず，pandasのDataFrameを使って金額を定義します．plansはプラン名の配列で，今後計算時のキーとして使います．

```python
import math
import datetime as dt
import pandas as pd

# plan names
plans = ['short', 'hour6', 'hour12', 'hour24', 'early', 'late', 'double']

# fare amount
a = {'base':[0, 4020, 6690, 8230, 2060, 2060, 2580], 
     'extra':[206, 206, 206, 206, 206, 206, 206],
     'distance':[0, 0, 16, 16, 16, 16, 16]}
amount = pd.DataFrame(data=a, index=plans)
```

続いて，各プランを計算するための条件もDataFrameにして，金額と結合します．

```python
# fare condition
c = {'type':['length', 'length', 'length', 'length', 'time', 'time', 'time'],
     'length':[0, 6, 12, 24, 6, 9, 15],
     'start':['', '', '', '', 18, 0, 18],
     'by':['', '', '', '', 24, 9, 24],
     'max':[72, 6, 6, 6, 6, 6, 6]}
condition = pd.DataFrame(data=c, index=plans)

fare = pd.concat([amount, condition], axis=1)
```

ちょっと分かりづらいですが，各列の意味は次のようにしました．

|列名 | 説明|
|:---:|:---|
|type|プランが時間の長さを持つか(length)，開始時刻と終了時刻を持つか(time)|
|length|typeがlengthの場合は時間の長さ，timeの場合はパックの開始時刻〜終了時刻まで．単位は[hour]|
|start|typeがtimeの場合のパックの開始時刻|
|by   |typeがtimeの場合，利用開始時刻はstart以降で，byよりも前でなければならない|
|max  |延長時間の上限．単位は[hour]|

## 料金を計算するクラス

「計算機(Calculator)」を表すクラスを１つ作ります．コンストラクタに利用開始時刻，返却予定時刻，距離をセットし，calc_fareメソッドにプラン名を与えると，料金を計算します．なお，下記のような場合にはmath.infを返します．

* プランのtypeがtimeで，かつ，そのプランが使えない時間帯である
* 延長時間が上限を超えている

```python
class FareCalculator():
    def __init__(self, start, end, distance):
        self.s = start
        self.e = end
        self.d = distance
        
    def __calc_extra(self, plan):
        if fare.at[plan, 'type'] == 'length':
            extra = self.e - self.s - dt.timedelta(
                hours=int(fare.at[plan, 'length']))
        elif fare.at[plan, 'type'] == 'time':
            plan_start = dt.datetime(year=self.s.year, 
                                     month=self.s.month, 
                                     day=self.s.day, 
                                     hour=fare.at[plan, 'start'])
            plan_end = plan_start + dt.timedelta(
                hours=int(fare.at[plan, 'length']))
            extra = self.e - plan_end
        else:
            extra = math.inf
        return max(0, math.ceil((extra.total_seconds()/3600)*4))

    def calc_fare(self, plan):
        if (fare.at[plan, 'type'] == 'time') and (
                self.s.hour < fare.at[plan, 'start'] or
                self.s.hour >= fare.at[plan, 'by']):
            return math.inf
        else:
            extra_qnum = self.__calc_extra(plan)
        if extra_qnum > fare.at[plan, 'max']*4:
            return math.inf
        else:
            return (fare.at[plan, 'base'] + 
                    fare.at[plan, 'extra'] * extra_qnum + 
                    fare.at[plan, 'distance'] * self.d)
```

__calc_extraメソッドについて少し説明します．

このメソッドは，延長時間を15分単位で計算するためのものです（例えば延長時間が1時間なら，戻り値は4）．延長時間の計算方法は，typeがlengthかtimeかで変わります．どちらにしても，datetime.timedelta型で延長時間(extra)を求めて，total_secondsプロパティを3600で割って時間[hour]にして，さらに4を乗じて15分単位に直しています．

if文を使って色々と処理をしちゃってますが，まあとりあえず良しとします．

## 計算してみる

これで料金を計算する準備が整いました．利用開始時刻，返却予定時刻，距離を与えてみましょう．例として，

* 利用開始時刻 2018年8月15日 15:30
* 返却予定時刻 2018年8月16日 08:00
* 距離 120km

を与えます．時刻はdatetime.datetime型を利用します．

```python
start = dt.datetime(year=2018, month=8, day=15, hour=15, minute=30)
end = dt.datetime(year=2018, month=8, day=16, hour=8, minute=0)
distance = 120
```

これをFareCalculatorクラスに与えた後，各プランの料金を計算して，最安値となるプランを選びます．結果はresultに書き込むことにします．

```python
calculator = FareCalculator(start, end, distance)
result = pd.DataFrame(columns=[], index=plans)
for plan in plans:
    result.at[plan, 'amount'] = calculator.calc_fare(plan)
```

最安プランの選択には，pandasのidxminを使います．

```python
print('Plan:', result.amount.idxmin(), ', Amount: ', result.amount.min())
```
答えはこんな感じになります．
Plan: hour24 , Amount:  10150.0

## 環境
Python 3.6.3
pandas 0.20.3

# 終わりに
Pythonの勉強がてら書いてみました．今後は別の勉強も兼ねて，WEBサービスにでもして公開してみたいと思っています[^2]．

[^1]: 組み合わせが可能なら，最適化問題の良い題材になりそうです．
[^2]: 公式サイトに最安プラン計算機能があっても良いと思うのですが．