Create costs.py
Browse files- services/costs.py +106 -0
services/costs.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import Optional, Union, Dict, Any
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
Number = Union[int, float]
|
| 6 |
+
|
| 7 |
+
def _parse_fare_hint(fare_hint: Any) -> Optional[float]:
|
| 8 |
+
"""
|
| 9 |
+
Navitia等からの運賃ヒントをなるべく数値(JPY)にして返す。
|
| 10 |
+
- dict: {"total": {"value": 420, "currency": "JPY"}} などを優先
|
| 11 |
+
{"amount": 420, "currency": "JPY"} にも対応
|
| 12 |
+
- str : "¥420", "JPY 420", "420 JPY" 等から数値を抽出
|
| 13 |
+
- それ以外: None
|
| 14 |
+
"""
|
| 15 |
+
# dictパターン
|
| 16 |
+
if isinstance(fare_hint, dict):
|
| 17 |
+
# Navitia: fare.total.value / fare.total.currency
|
| 18 |
+
total = fare_hint.get("total") if isinstance(fare_hint.get("total"), dict) else None
|
| 19 |
+
if total and isinstance(total.get("value"), (int, float)):
|
| 20 |
+
cur = str(total.get("currency") or "JPY").upper()
|
| 21 |
+
if cur in ("JPY", "YEN", "¥", "JPY "):
|
| 22 |
+
return float(total["value"])
|
| 23 |
+
else:
|
| 24 |
+
# 通貨が別の時はここでは換算せず None(過小評価を避ける)
|
| 25 |
+
return None
|
| 26 |
+
# amount/currency 形式
|
| 27 |
+
amt = fare_hint.get("amount")
|
| 28 |
+
cur = str(fare_hint.get("currency") or "JPY").upper()
|
| 29 |
+
if isinstance(amt, (int, float)) and cur in ("JPY", "YEN", "¥"):
|
| 30 |
+
return float(amt)
|
| 31 |
+
# 別のキーでも value を持っていればそれを採用
|
| 32 |
+
for k in ("value", "price", "fare"):
|
| 33 |
+
v = fare_hint.get(k)
|
| 34 |
+
if isinstance(v, (int, float)):
|
| 35 |
+
return float(v)
|
| 36 |
+
|
| 37 |
+
# 文字列パターン
|
| 38 |
+
if isinstance(fare_hint, str):
|
| 39 |
+
m = re.search(r"([\d,]+(?:\.\d+)?)", fare_hint)
|
| 40 |
+
if m:
|
| 41 |
+
try:
|
| 42 |
+
return float(m.group(1).replace(",", ""))
|
| 43 |
+
except Exception:
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def estimate_leg_cost(
|
| 50 |
+
mode: str,
|
| 51 |
+
distance_km: Number,
|
| 52 |
+
duration_min: Number,
|
| 53 |
+
fare_hint: Optional[Union[dict, str, Number]] = None,
|
| 54 |
+
) -> float:
|
| 55 |
+
"""
|
| 56 |
+
区間の推定交通費(JPY)を返す簡易関数。
|
| 57 |
+
- mode: "Walk" | "Transit" | "Drive/Taxi"
|
| 58 |
+
- fare_hint: Navitia等の運賃情報がある場合はそれを優先
|
| 59 |
+
- 返り値: 四捨五入して10円単位
|
| 60 |
+
"""
|
| 61 |
+
# ヒントがあれば最優先(数値化できればそのまま採用)
|
| 62 |
+
if isinstance(fare_hint, (int, float)):
|
| 63 |
+
return float(round(fare_hint / 10.0) * 10)
|
| 64 |
+
hint = _parse_fare_hint(fare_hint)
|
| 65 |
+
if hint is not None:
|
| 66 |
+
return float(round(hint / 10.0) * 10)
|
| 67 |
+
|
| 68 |
+
d = max(0.0, float(distance_km))
|
| 69 |
+
t = max(0.0, float(duration_min))
|
| 70 |
+
mode = (mode or "Walk").strip()
|
| 71 |
+
|
| 72 |
+
# 徒歩は無料
|
| 73 |
+
if mode.lower().startswith("walk"):
|
| 74 |
+
return 0.0
|
| 75 |
+
|
| 76 |
+
# 公共交通:ざっくり距離比例(東京近郊の初乗り含むイメージ)
|
| 77 |
+
if mode.lower().startswith("transit"):
|
| 78 |
+
base = 180.0 # 初乗りイメージ
|
| 79 |
+
per_km = 22.0 # 距離係数
|
| 80 |
+
est = base + per_km * d
|
| 81 |
+
# 長距離で上限(空港連絡等は別だが、概算としてのキャップ)
|
| 82 |
+
est = min(est, 1200.0)
|
| 83 |
+
return float(round(est / 10.0) * 10)
|
| 84 |
+
|
| 85 |
+
# タクシー/自動車:ざっくり距離+低速時の時間加算
|
| 86 |
+
# 東京の実勢に近い概算:初乗り500円 + 420円/km + 渋滞待機 45円/分(低速時のみ)
|
| 87 |
+
if mode.lower().startswith("drive"):
|
| 88 |
+
flag_fall = 500.0
|
| 89 |
+
per_km = 420.0
|
| 90 |
+
est = flag_fall + per_km * d
|
| 91 |
+
|
| 92 |
+
# 低速(12km/h未満)なら待機料金を加算(概算)
|
| 93 |
+
speed_kmh = (d / (t / 60.0)) if t > 0 else 999.0
|
| 94 |
+
if speed_kmh < 12.0:
|
| 95 |
+
wait_fee_per_min = 45.0
|
| 96 |
+
# 12km/h で同距離を走るのに要する時間との差を「渋滞/待機」とみなす
|
| 97 |
+
ideal_min = (d / 12.0) * 60.0
|
| 98 |
+
extra_min = max(0.0, t - ideal_min)
|
| 99 |
+
est += wait_fee_per_min * extra_min
|
| 100 |
+
|
| 101 |
+
# 短距離の過小評価を避ける最低料金
|
| 102 |
+
est = max(est, 680.0)
|
| 103 |
+
return float(round(est / 10.0) * 10)
|
| 104 |
+
|
| 105 |
+
# 未知のモードは0円にしておく(上流で弾かれる想定)
|
| 106 |
+
return 0.0
|