Skip to content

Conversation

@TinyKitten
Copy link
Member

@TinyKitten TinyKitten commented Jan 17, 2026

  • get_routes/get_routes_minimalにバス時刻表フィルタリングを実装
  • 現在のJST時刻で±30分以内に発車予定がないバス停は
    stop_condition=Notを返すように変更
  • StationRepositoryにget_active_bus_stop_station_cdsメソッドを追加
  • gtfs_calendar/gtfs_calendar_datesを使用して曜日別の
    サービス運行状況を考慮
  • ユニットテストを追加(get_current_jst、バス停判定など)

Summary by CodeRabbit

  • 新機能

    • JST現在日時を用いて、指定した駅コード群から稼働中のバス停を判定・抽出するフィルタを追加(時刻窓±30分・曜日・カレンダー考慮)。駅情報更新・鉄道+バス集約・経路生成で非稼働バス停を自動除外・状態反映。
    • GTFSの停留所IDを駅データに保存する仕組みと索引を追加し、取り込み処理で停留所IDを格納。
  • テスト

    • 新フィルタ向けの単体・統合テストとモックを追加。時刻・曜日処理、混在入力、空入力、鉄道除外、関連処理への反映を検証。

✏️ Tip: You can customize this high-level summary in your review settings.

- get_routes/get_routes_minimalにバス時刻表フィルタリングを実装
- 現在のJST時刻で±30分以内に発車予定がないバス停は
  stop_condition=Notを返すように変更
- StationRepositoryにget_active_bus_stop_station_cdsメソッドを追加
- gtfs_calendar/gtfs_calendar_datesを使用して曜日別の
  サービス運行状況を考慮
- ユニットテストを追加(get_current_jst、バス停判定など)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

StationRepository にバス停の「アクティブ」判定を行う非同期メソッドを追加し、インフラでGTFSベースのSQL実装を導入。ユースケースで JST 時刻算出を追加して取得結果に基づくバス停フィルタを適用、GTFS stop_id の永続化と関連テストを追加。

Changes

コホート / ファイル(s) 変更内容
ドメインリポジトリ定義
stationapi/src/domain/repository/station_repository.rs
トレイトに async fn get_active_bus_stop_station_cds(&self, station_cds: &[i32], current_time_jst: &str, current_date_jst: &str) -> Result<std::collections::HashSet<i32>, DomainError> を追加。モック/テストの拡張を追加。
インフラ実装(リポジトリ)
stationapi/src/infrastructure/station_repository.rs
MyStationRepository/InternalStationRepositoryget_active_bus_stop_station_cds を追加。空入力早期リターン、JST 日付→曜日判定、±30分ウィンドウ計算、GTFS ベースの SQL 実行でアクティブ station_cd 集合を返却する実装を追加。
ユースケース(インタラクタ)
stationapi/src/use_case/interactor/query.rs
プライベート get_current_jst() を追加。取得した JST 時刻でリポジトリを呼び出し、結果に基づきバス停の StopCondition::Not を複数フロー(駅更新、経路構築、RailAndBus の周辺バス統合等)へ適用。関連テストとモックを追加。
DB スキーマ
data/create_table.sql
public.stationsgtfs_stop_id TEXT カラムとインデックス idx_stations_gtfs_stop_id を追加。
GTFS インポート
stationapi/src/import.rs
GTFS stop の stop_idgtfs_stop_id にバインドして挿入するように INSERT 文を更新。
テスト & モック補助
.../tests/*, stationapi/src/...
バス停用テストデータと create_test_bus_stop ヘルパーを追加。モックで get_active_bus_stop_station_cds を実装し、鉄道除外・混在入力・空入力などのケースを追加。

Sequence Diagram

sequenceDiagram
    participant Client
    participant UseCase as Use Case
    participant Repo as StationRepository
    participant DB as GTFS_DB

    Client->>UseCase: ルート/駅情報要求
    UseCase->>UseCase: get_current_jst() を計算(YYYYMMDD, HH:MM:SS)
    Note over UseCase: 対象の bus stop station_cds を収集
    UseCase->>Repo: get_active_bus_stop_station_cds(station_cds, time, date)
    Repo->>DB: 実行(SQL: services by weekday/calendar → stop_times ±30min → stations.gtfs_stop_id join)
    DB-->>Repo: アクティブな station_cds の集合
    Repo-->>UseCase: HashSet<station_cd>
    UseCase->>UseCase: 非アクティブは stop_condition = Not に設定
    UseCase-->>Client: フィルタ済み結果を返却
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • 10mocy

Poem

🐰 朝の草むらでぴょんと跳ねて
JST の光で時をはかるよ
SQLの小道をしっぽで辿り
生きてるバス停だけを集めるよ
発車ベルに合わせてまた跳ぶよ

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRのタイトルは、バス時刻表ベースのstop_conditionフィルタリング機能追加という変更内容を適切に要約しており、主要な変更を明確に反映しています。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

get_routes/get_routes_minimal以外のAPIエンドポイントにも
バス時刻表フィルタリングを適用

対象メソッド:
- get_station_by_id
- get_stations_by_id_vec
- get_stations_by_group_id
- get_stations_by_coordinates
- get_stations_by_line_id
- get_stations_by_name
- get_stations_by_line_group_id
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@stationapi/src/domain/repository/station_repository.rs`:
- Around line 90-94: フォーマットエラーが出ているのでリポジトリの Rust コードを整形してください:
station_repository.rs 内のテストデータ追加ブロック(create_test_bus_stop と stations.insert
を使っている箇所)を含めて、プロジェクトルートで cd stationapi && cargo fmt --all
を実行して自動整形し、フォーマット差分をコミットしてください(CI 用には cargo fmt --all -- --check
をローカルで確認してからコミットすること)。

In `@stationapi/src/infrastructure/station_repository.rs`:
- Around line 1572-1599: Run rustfmt (cargo fmt) to fix formatting issues in the
time-window calculation block that defines current_time_jst, time_parts, hours,
minutes, total_minutes, start_minutes, end_minutes, start_time and end_time;
ensure spacing, line breaks, and alignment follow rustfmt rules (e.g.,
consistent indentation and wrapping for the chained parse/unwrap_or calls and
the format! macros) and commit the formatted file.
- Around line 1650-1663: Format error around the HashSet construction should be
fixed by running rustfmt; from the repository root run the suggested command (cd
stationapi && cargo fmt --all) to reformat code where StationCdRow/rows/result
are defined, then commit the changes so the block that builds result:
std::collections::HashSet<i32> using rows.into_iter().filter_map(|r|
r.station_cd).collect() is properly formatted.

In `@stationapi/src/use_case/interactor/query.rs`:
- Around line 667-721: The file has formatting issues detected by CI; run
rustfmt/cargo fmt and reformat the changes around the block that constructs
proto_station (references: tt_line_map, TrainType creation,
build_station_from_row, proto::StopCondition) so the if/else indentation and
method chains (tt_lines.iter().filter(...).cloned().collect()) conform to
rustfmt style; after running cargo fmt, stage the updated file and push the
formatted changes.
🧹 Nitpick comments (2)
stationapi/src/use_case/interactor/query.rs (2)

753-769: get_routesget_routes_minimalでバス停フィルタリングロジックが重複しています。

同じロジックが2箇所にあるため、将来的にはヘルパーメソッドに抽出することを検討してください。現時点では動作に問題はありませんが、保守性の観点から改善の余地があります。

♻️ ヘルパーメソッド抽出の例
async fn get_active_bus_stops_for_route(&self, stops: &[Station]) -> HashSet<i32> {
    let bus_station_cds: Vec<i32> = stops
        .iter()
        .filter(|s| s.transport_type == TransportType::Bus)
        .map(|s| s.station_cd)
        .collect();

    if bus_station_cds.is_empty() {
        return HashSet::new();
    }

    let (current_date, current_time) = get_current_jst();
    self.station_repository
        .get_active_bus_stop_station_cds(&bus_station_cds, &current_time, &current_date)
        .await
        .unwrap_or_default()
}

6-14: chrono::TimeDelta::hoursの使用を推奨します。

chrono::Duration::hoursは現在のバージョンで正式には非推奨ではありませんが、最新のchronoではより推奨されるアプローチはchrono::TimeDelta::hoursを使用することです。将来のバージョン互換性のため、Duration::hours(9)TimeDelta::hours(9)に置き換えることを検討してください。

また、この関数はテスト時にモック化が困難です。将来的に、時刻をパラメータとして受け取る設計(例:fn get_current_jst(now: DateTime<Utc>) -> (String, String))も検討できます。

鉄道駅の近くにあるバス路線(nearby_bus_lines)に含まれる
バス停についても時刻表フィルタリングを適用

- nearby_bus_linesを事前に収集し、バス停station_cdを含める
- 一度のget_active_bus_stop_station_cds呼び出しで
  すべてのバス停の運行状況を取得
- 運行していないバス停のstop_conditionをNotに設定
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@stationapi/src/use_case/interactor/query.rs`:
- Around line 318-326: 呼び出し元で .unwrap_or_default() によって
get_active_bus_stop_station_cds
のエラーを飲み込んでいるため、失敗時に何が起きたか分からなくなっています。active_bus_stops を生成する箇所で
station_repository.get_active_bus_stop_station_cds(...) の結果を match して Err(_)
の場合は適切なロガー(例: tracing::error! または
self.logger.error)でエラー内容を出力し、続けて安全なデフォルト(HashSet::new())を返すように修正してください;参照箇所:
get_active_bus_stop_station_cds, active_bus_stops,
station_repository(get_current_jst 呼び出しはそのまま利用)。
- Around line 877-884: The stop_condition computation is inconsistent between
get_routes and get_routes_minimal: update get_routes_minimal to use
row.stop_condition (converted from the StopCondition enum) instead of row.pass
so both functions derive stop_condition from the same source; specifically, in
get_routes_minimal replace the current logic that uses row.pass with logic
equivalent to get_routes (use row.stop_condition as i32, but still override to
proto::StopCondition::Not as i32 when row.transport_type == TransportType::Bus
&& !active_bus_stops.contains(&row.station_cd)). Ensure you reference the same
fields (row.stop_condition, TransportType::Bus, active_bus_stops, and
proto::StopCondition::Not) and keep the conversion to i32 consistent with
get_routes.
♻️ Duplicate comments (1)
stationapi/src/use_case/interactor/query.rs (1)

719-773: フォーマットの問題を修正してください。

パイプラインでフォーマットエラーが検出されています。cargo fmtを実行してください。

🧹 Nitpick comments (3)
stationapi/src/use_case/interactor/query.rs (3)

289-326: バス停フィルタリングロジックの重複があります。

同じパターンが3箇所(update_station_vec_with_attributesget_routesget_routes_minimal)で繰り返されています。ヘルパーメソッドへの抽出を検討してください。

♻️ リファクタリング案
/// Helper to get active bus stops from a list of stations
async fn get_active_bus_stops_for_stations(
    &self,
    stations: &[Station],
) -> HashSet<i32> {
    let bus_station_cds: Vec<i32> = stations
        .iter()
        .filter(|s| s.transport_type == TransportType::Bus)
        .map(|s| s.station_cd)
        .collect();

    if bus_station_cds.is_empty() {
        return HashSet::new();
    }

    let (current_date, current_time) = get_current_jst();
    self.station_repository
        .get_active_bus_stop_station_cds(&bus_station_cds, &current_time, &current_date)
        .await
        .unwrap_or_else(|e| {
            tracing::warn!("Failed to get active bus stops: {}", e);
            HashSet::new()
        })
}

Also applies to: 674-690, 805-821


721-755: matchif に置き換えることで可読性が向上します。

match row.type_id.is_some()if let Some(_) = row.type_id または単純な if row.type_id.is_some() に書き換えることで、より自然なRustイディオムになります。

♻️ リファクタリング案
-                            let train_type = match row.type_id.is_some() {
-                                true => {
+                            let train_type = if row.type_id.is_some() {
                                     // Filter lines to only include those with matching line_group_cd
                                     // and remove duplicates by line_cd
                                     let mut seen_line_cds = std::collections::HashSet::new();
                                     let filtered_lines: Vec<Line> = tt_lines
                                         .iter()
                                         .filter(|line| {
                                             row.line_group_cd.is_some()
                                                 && line.line_group_cd == row.line_group_cd
                                                 && seen_line_cds.insert(line.line_cd)
                                         })
                                         .cloned()
                                         .collect();

                                     Some(Box::new(TrainType {
                                         id: row.type_id,
                                         station_cd: Some(row.station_cd),
                                         type_cd: row.type_cd,
                                         line_group_cd: row.line_group_cd,
                                         pass: row.pass,
                                         type_name: row.type_name.clone().unwrap_or_default(),
                                         type_name_k: row.type_name_k.clone().unwrap_or_default(),
                                         type_name_r: row.type_name_r.clone(),
                                         type_name_zh: row.type_name_zh.clone(),
                                         type_name_ko: row.type_name_ko.clone(),
                                         color: row.color.clone().unwrap_or_default(),
                                         direction: row.direction,
                                         kind: row.kind,
                                         line: Some(Box::new(tt_line.clone())),
                                         lines: filtered_lines,
                                     }))
-                                }
-                                false => None,
-                            };
+                            } else {
+                                None
+                            };

6-14: 関数内の use 文をファイル先頭のインポートに統合することを検討してください。

chrono::Duration::hours() は現在サポートされているバージョン(0.4.20+)で動作しますが、chrono 0.4.26 以降では DurationTimeDelta の型エイリアスとなっています。最新のベストプラクティスに従うため、TimeDelta::hours() を使用する方が望ましいです。ただし、現在のコードは機能的には問題ありません。

- get_active_bus_stop_station_cdsのエラー時にtracing::warnでログ出力
- get_routes_minimalでrow.passではなくrow.stop_conditionを使用し
  get_routesとの一貫性を確保
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@stationapi/src/use_case/interactor/query.rs`:
- Around line 6-14: The get_current_jst function imports and uses
chrono::Duration which is being renamed to chrono::TimeDelta; update the import
and usage to avoid deprecation: replace use chrono::{Duration, Utc}; with use
chrono::{TimeDelta, Utc}; and change Duration::hours(9) to TimeDelta::hours(9)
inside get_current_jst so the code uses TimeDelta::hours(9) when computing
jst_now.
🧹 Nitpick comments (1)
stationapi/src/use_case/interactor/query.rs (1)

684-710: アクティブバス停取得ロジックの重複を検討してください。

get_active_bus_stop_station_cds の呼び出しパターンが update_station_vec_with_attributesget_routesget_routes_minimal の3箇所で重複しています。ヘルパーメソッドへの抽出を検討できますが、各呼び出し箇所で微妙にコンテキストが異なるため、現状のままでも問題ありません。

♻️ ヘルパーメソッド抽出の例
async fn get_active_bus_stops_for_stations(
    &self,
    bus_station_cds: &[i32],
) -> HashSet<i32> {
    if bus_station_cds.is_empty() {
        return HashSet::new();
    }
    
    let (current_date, current_time) = get_current_jst();
    match self
        .station_repository
        .get_active_bus_stop_station_cds(bus_station_cds, &current_time, &current_date)
        .await
    {
        Ok(stops) => stops,
        Err(e) => {
            tracing::warn!(
                "Failed to get active bus stop station_cds: {}. Using empty set as fallback.",
                e
            );
            HashSet::new()
        }
    }
}

chrono::Durationは非推奨のため、TimeDeltaを使用するように変更
TimeDelta::hours()がOption<TimeDelta>を返すようになったため、
FixedOffset::east_opt()を使用した正しいタイムゾーン変換に変更
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@stationapi/src/use_case/interactor/query.rs`:
- Around line 338-345: The new bus-stop filtering only updates the top-level
station.stop_condition, causing station.lines[*].station.stop_condition to stay
stale; update the same Not-setting logic for the nested copy used for lines (the
variable named station_copy or similar) whenever station.transport_type ==
TransportType::Bus and station.station_cd is not in active_bus_stops so that
both station.stop_condition and station_copy.lines[i].station.stop_condition are
set to crate::proto::StopCondition::Not; locate the loop over stations
(function/query handling where stations.iter_mut() is used) and apply the
identical conditional assignment to the nested station copy used for lines to
keep display state consistent.

update_station_vec_with_attributes内でlines配列に設定される
ネストされたstation_copyにもバス時刻表フィルタリングを適用し、
トップレベルのstation.stop_conditionと一貫性を保つ
- station_repository.rsにuse chrono::Datelike;を追加してweekday()メソッドを使用可能に
- create_test_bus_stop()にkindパラメータを追加
- test_get_by_line_group_idの期待値を4から6に更新(バス停を含む)
active_bus_stopsが空の場合(バス運行時間外)はフィルタリングをスキップし、
元のstop_conditionを維持するように変更。
問題: 同じバス停が各路線ごとに異なるstation_cdを持つため、
gtfs_stops.station_cdとのマッピングが正しく機能していなかった。

解決策:
- stationsテーブルにgtfs_stop_idカラムを追加
- インポート処理でgtfs_stop_idを設定
- SQLクエリをgtfs_stop_id経由でJOINするように変更
- 親バス停と子バス停(ポール)の両方に対応
@TinyKitten TinyKitten requested a review from 10mocy as a code owner January 17, 2026 12:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@stationapi/src/use_case/interactor/query.rs`:
- Around line 290-349: The code currently treats an error and a genuine empty
result identically for active_bus_stops, causing "no filter" behavior when there
are actually zero departures; change active_bus_stops from a HashSet to an
Option<HashSet<i32>> so that on Ok(stops) you return Some(stops) (including
Some(empty) for zero results) and on Err(e) you return None (and log the error)
— adjust consumers of active_bus_stops accordingly to apply the filter when
Option::Some is present and skip filtering only when None; update the match
around station_repository.get_active_bus_stop_station_cds, the variable
declaration for active_bus_stops, and any uses in
update_station_vec_with_attributes / related get_routes/get_routes_minimal flows
to follow this semantics.

Comment on lines 290 to 349
// Collect all bus stop station_cds for timetable filtering
let mut bus_station_cds: Vec<i32> = stations
.iter()
.filter(|s| s.transport_type == TransportType::Bus)
.map(|s| s.station_cd)
.collect();

// For RailAndBus, also collect nearby bus stops from rail stations
let should_include_bus_routes = transport_type == TransportTypeFilter::RailAndBus;
let mut nearby_bus_lines_map: std::collections::HashMap<i32, Vec<Line>> =
std::collections::HashMap::new();

if should_include_bus_routes {
for station in stations.iter() {
if station.transport_type == TransportType::Rail {
let nearby_bus_lines =
self.get_nearby_bus_lines(station.lat, station.lon).await?;
// Collect bus stop station_cds from nearby bus lines
for bus_line in &nearby_bus_lines {
if let Some(ref bus_stop) = bus_line.station {
bus_station_cds.push(bus_stop.station_cd);
}
}
nearby_bus_lines_map.insert(station.station_cd, nearby_bus_lines);
}
}
}

// Get active bus stops based on current JST time
let active_bus_stops = if !bus_station_cds.is_empty() {
let (current_date, current_time) = get_current_jst();
tracing::debug!(
"update_station_vec_with_attributes: bus_station_cds.len()={}, date={}, time={}",
bus_station_cds.len(),
current_date,
current_time
);
match self
.station_repository
.get_active_bus_stop_station_cds(&bus_station_cds, &current_time, &current_date)
.await
{
Ok(stops) => {
tracing::debug!(
"update_station_vec_with_attributes: active_bus_stops.len()={}",
stops.len()
);
stops
}
Err(e) => {
tracing::warn!(
"Failed to get active bus stop station_cds: {}. Using empty set as fallback.",
e
);
HashSet::new()
}
}
} else {
HashSet::new()
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

active_bus_stops が空のときに全停留所が Not にならず、要件と乖離します。
現在は「空集合=フィルタ適用なし」扱いのため、出発予定がゼロの時間帯でも bus 停留所が Not になりません。エラー時のフォールバックと「実際にゼロ件」の区別が必要です。

💡 例: エラーと空集合を区別して適用する案
-        let active_bus_stops = if !bus_station_cds.is_empty() {
+        let active_bus_stops = if !bus_station_cds.is_empty() {
             let (current_date, current_time) = get_current_jst();
             match self
                 .station_repository
                 .get_active_bus_stop_station_cds(&bus_station_cds, &current_time, &current_date)
                 .await
             {
-                Ok(stops) => stops,
+                Ok(stops) => Some(stops),
                 Err(e) => {
                     tracing::warn!(
                         "Failed to get active bus stop station_cds: {}. Using empty set as fallback.",
                         e
                     );
-                    HashSet::new()
+                    None
                 }
             }
         } else {
-            HashSet::new()
+            None
         };

-            if !active_bus_stops.is_empty()
-                && station.transport_type == TransportType::Bus
-                && !active_bus_stops.contains(&station.station_cd)
-            {
-                station.stop_condition = crate::proto::StopCondition::Not;
-            }
+            if let Some(active_bus_stops) = &active_bus_stops {
+                if station.transport_type == TransportType::Bus
+                    && !active_bus_stops.contains(&station.station_cd)
+                {
+                    station.stop_condition = crate::proto::StopCondition::Not;
+                }
+            }

get_routes / get_routes_minimal / 近傍バス停反映も同様の扱いに揃える想定です。

Also applies to: 351-359, 392-405, 427-434, 710-736, 811-821, 853-880, 935-955

🤖 Prompt for AI Agents
In `@stationapi/src/use_case/interactor/query.rs` around lines 290 - 349, The code
currently treats an error and a genuine empty result identically for
active_bus_stops, causing "no filter" behavior when there are actually zero
departures; change active_bus_stops from a HashSet to an Option<HashSet<i32>> so
that on Ok(stops) you return Some(stops) (including Some(empty) for zero
results) and on Err(e) you return None (and log the error) — adjust consumers of
active_bus_stops accordingly to apply the filter when Option::Some is present
and skip filtering only when None; update the match around
station_repository.get_active_bus_stop_station_cds, the variable declaration for
active_bus_stops, and any uses in update_station_vec_with_attributes / related
get_routes/get_routes_minimal flows to follow this semantics.

SQLクエリを修正して、stations.line_cd → gtfs_routes → gtfs_trips の
関係を使い、特定の路線に属するバス停のみをフィルタリング対象に。
これにより、別路線が停車するバス停が誤ってアクティブと判定される
問題を解決。
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@stationapi/src/infrastructure/station_repository.rs`:
- Around line 1573-1626: The time-window logic in
current_time_jst/total_minutes/start_minutes/end_minutes clamps across midnight
and misses previous/next-day GTFS trips and 24+:xx departure_time values; update
the logic to compute un-clamped start_minutes and end_minutes (allowing negative
and >=1440) and split query into up to two/three parts covering previous-day
(when start_minutes<0, map to 24:.. by adding 1440 and query with date =
prev_date), current-day (date = date), and next-day (when end_minutes>=1440, map
to 24:.. by subtracting 1440 and query with date = next_date), then UNION those
SELECTs (or run separate queries) to collect station_cd results and dedupe with
a HashSet; modify the SQL filters that compare gst.departure_time to accept
times like '24:30:00' (use string bounds matching the mapped 24+:xx values) and
adjust parameters ($1, $2, $3, $4) accordingly so
gtfs_calendar/gtfs_calendar_dates are consulted for the correct date per
subquery (refer to variables current_time_jst, start_minutes, end_minutes and
the query string).

Comment on lines +1573 to +1626
// Calculate time window (±30 minutes)
let time_parts: Vec<&str> = current_time_jst.split(':').collect();
let hours: i32 = time_parts.first().unwrap_or(&"0").parse().unwrap_or(0);
let minutes: i32 = time_parts.get(1).unwrap_or(&"0").parse().unwrap_or(0);

// Calculate start and end times for the 30-minute window
let total_minutes = hours * 60 + minutes;
let start_minutes = (total_minutes - 30).max(0);
let end_minutes = total_minutes + 30;

let start_time = format!("{:02}:{:02}:00", start_minutes / 60, start_minutes % 60);
let end_time = format!("{:02}:{:02}:00", end_minutes / 60, end_minutes % 60);

// Query to find active bus stops:
// 1. Find service_ids that are active today based on gtfs_calendar and gtfs_calendar_dates
// 2. Find trips with those service_ids AND matching route (via line_cd)
// 3. Find stop_times within the time window
// 4. Map stop_ids to station_cds via stations.gtfs_stop_id
// (handles parent/child stop relationships)
let query = format!(
r#"
WITH active_services AS (
-- Services that run on this day of week
SELECT gc.service_id
FROM gtfs_calendar gc
WHERE gc.{day_column} = true
AND gc.start_date <= $1
AND gc.end_date >= $1
-- Add exceptions (service added)
UNION
SELECT gcd.service_id
FROM gtfs_calendar_dates gcd
WHERE gcd.date = $1
AND gcd.exception_type = 1
-- Remove exceptions (service removed)
EXCEPT
SELECT gcd.service_id
FROM gtfs_calendar_dates gcd
WHERE gcd.date = $1
AND gcd.exception_type = 2
)
SELECT DISTINCT s.station_cd
FROM stations s
JOIN gtfs_routes gr ON gr.line_cd = s.line_cd
JOIN gtfs_trips gt ON gt.route_id = gr.route_id
JOIN active_services ase ON ase.service_id = gt.service_id
JOIN gtfs_stop_times gst ON gst.trip_id = gt.trip_id
JOIN gtfs_stops gs ON gs.stop_id = gst.stop_id
WHERE s.station_cd = ANY($4)
AND s.gtfs_stop_id IS NOT NULL
AND (gs.stop_id = s.gtfs_stop_id OR gs.parent_station = s.gtfs_stop_id)
AND gst.departure_time >= $2
AND gst.departure_time <= $3
"#,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find stationapi/src/infrastructure -name "*.rs" | head -20

Repository: TrainLCD/StationAPI

Length of output: 358


🏁 Script executed:

wc -l stationapi/src/infrastructure/station_repository.rs

Repository: TrainLCD/StationAPI

Length of output: 119


🏁 Script executed:

sed -n '1570,1630p' stationapi/src/infrastructure/station_repository.rs

Repository: TrainLCD/StationAPI

Length of output: 2779


🏁 Script executed:

# Find GTFS schema/migration files
find . -type f \( -name "*.sql" -o -name "*.rs" \) | xargs grep -l "gtfs_stop_times" | head -10

Repository: TrainLCD/StationAPI

Length of output: 218


🏁 Script executed:

# Look for schema definitions
find . -type f -name "*.sql" | xargs grep -l "departure_time" 2>/dev/null | head -5

Repository: TrainLCD/StationAPI

Length of output: 86


🏁 Script executed:

cat ./data/create_table.sql | grep -A 50 "gtfs_stop_times"

Repository: TrainLCD/StationAPI

Length of output: 4093


🏁 Script executed:

# Get more context on the function - find its full signature
sed -n '1500,1630p' stationapi/src/infrastructure/station_repository.rs | head -100

Repository: TrainLCD/StationAPI

Length of output: 4273


🏁 Script executed:

# Search for function signature
rg "get_active_bus_stop_station_cds" -B 10 -A 2

Repository: TrainLCD/StationAPI

Length of output: 18500


🏁 Script executed:

# Check if there's any date boundary logic elsewhere
rg "start_minutes|end_minutes" stationapi/

Repository: TrainLCD/StationAPI

Length of output: 551


🏁 Script executed:

# Check tests for this function
rg "get_active_bus_stop_station_cds" stationapi/ -A 5 -B 5

Repository: TrainLCD/StationAPI

Length of output: 16425


深夜帯(00:10、23:50など)で日跨ぎ±30分が欠落し、バス停が誤判定になります。

start_minutes = (total_minutes - 30).max(0) でクランプされるため、例えば00:10時点では window が [00:00, 00:40] となり、前日の 23:40-23:59 の便が落ちます。また、23:50 では window が [23:20, 24:20] となりますが、次の日のサービスはクエリされません。スキーマからも確認されるように、GTFS では departure_time が "25:30:00" のように 24:xx 表記を許容していることに対応していません。

前日/翌日のサービスも含めて検索する必要があります。例えば日跨ぎ時は、前日の 24:xx ウィンドウと翌日の 00:xx ウィンドウを UNION で統合し、結果を HashSet で統合する方法が考えられます。

🤖 Prompt for AI Agents
In `@stationapi/src/infrastructure/station_repository.rs` around lines 1573 -
1626, The time-window logic in
current_time_jst/total_minutes/start_minutes/end_minutes clamps across midnight
and misses previous/next-day GTFS trips and 24+:xx departure_time values; update
the logic to compute un-clamped start_minutes and end_minutes (allowing negative
and >=1440) and split query into up to two/three parts covering previous-day
(when start_minutes<0, map to 24:.. by adding 1440 and query with date =
prev_date), current-day (date = date), and next-day (when end_minutes>=1440, map
to 24:.. by subtracting 1440 and query with date = next_date), then UNION those
SELECTs (or run separate queries) to collect station_cd results and dedupe with
a HashSet; modify the SQL filters that compare gst.departure_time to accept
times like '24:30:00' (use string bounds matching the mapped 24+:xx values) and
adjust parameters ($1, $2, $3, $4) accordingly so
gtfs_calendar/gtfs_calendar_dates are consulted for the correct date per
subquery (refer to variables current_time_jst, start_minutes, end_minutes and
the query string).

@TinyKitten
Copy link
Member Author

やり直そう..

@TinyKitten TinyKitten closed this Jan 17, 2026
@TinyKitten TinyKitten deleted the claude/add-bus-timetable-filtering-BxpAf branch January 17, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants