0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「駅すぱあと API」をGoogle App Scriptから利用して、経費精算用に経路探索を行う

Posted at

本記事はヴァル研究所の支援として作成された記事です

今回は、「駅すぱあと API」をGoogleスプレッドシート(Google Apps Script)から利用して、経費精算に利用する情報を取得してみます。出発駅や経由、到着駅を選んで実行すると、ルートや運賃が表示されますので、経費精算の手間が大幅に軽減されるでしょう。

駅すぱあと APIの基本的な利用方法は前回の経路探索の基本:経費精算を自動化する簡単な方法を参考にしてください。

できあがりイメージ

今回のGoogle Apps Scriptは、こちらのリポジトリにアップロードしてあります。駅すぱあとAPIのAPIキーは自分が取得したものを利用してください。

駅名・バス停名などの入力補完

image4.jpg

今回は全部で4つの経路情報が入力できます。出発駅と到着駅は必須です。入力すると、駅名が入力されたセルに対して、入力補完が行われます。

  • 出発駅
  • 経由駅1
  • 経由駅2
  • 到着駅

たとえば 八王子 と入力すると、横浜に一致する駅名やバス停などが選べるようになります。これは駅簡易情報APIを利用しています。

この入力補完用データは、データシートに書き込まれます。

image3.png

経路検索

出発駅や経由駅を入力した後、Ekispert メニューから ルート探索 を選択すると、経路探索を実行します。これは経路探索APIを利用しています。

結果は、別のシートが作成され、そのシートへルート毎・横並びに表示されます。経由した駅名、路線名、そして最終的な運賃が算出されます。

image6.jpg

実装について

駅名・バス停名などの入力補完機能の実装

入力補完を行う際には、シート編集時のトリガーを利用して、セルの入力イベントをチェックします。onEditではネットワークアクセスができないので注意してください。今回は onEditHandler という関数を作成し、その関数に編集時のトリガーを設定しています。

image5.jpg

ここで行っているのは、以下のような内容です。

  1. 入力されたセルが、出発駅、経由駅1、経由駅2、到着駅のいずれかであるかをチェック
  2. すでに入力補完(ドロップダウン)があり、そこからの選択であれば何もしない
  3. 削除された場合は、入力補完のデータ元(データシート)も削除
  4. 入力された内容で駅名を検索
  5. データシートに結果を書き込み、入力補完を設定

実際のコードです。以下は、入力イベントの処理です。

**
 * 編集時用関数駅名入力時に補完候補リストとドロップダウンを設定
 * @param {Object} e - onEdit イベントオブジェクト
 */
function onEditHandler(e) {
  const sheet = e.range.getSheet();
  if (sheet.getName() !== '経路入力') return; // 別シートでは実行しない

  const row = e.range.getRow();
  const col = e.range.getColumn();
  if (col !== 2 || row < 1 || row > 4) return; // B1〜B4のみ対象

  const val = e.value;
  const dataSh = SpreadsheetApp.getActive().getSheetByName('データ');
  console.log(`編集: row=${row}, 値=${val}`);

  // 入力行に対応した列位置を計算:B1→A,B / B2→C,D / ...
  const startCol = (row - 1) * 2 + 1;

  if (!val) {
    e.range.clearDataValidations(); // ドロップダウン解除
    // 該当セル範囲をクリア
    dataSh.getRange(2, startCol, dataSh.getMaxRows() - 1, 2).clearContent();
    return;
  }

  const pts = fetchStations(val); // APIから駅情報取得
  const names = pts.map(p => p.Station.Name); // 駅名のみリスト化

  // 駅名・コードを縦に書き込み
  const output = pts.map(p => [p.Station.Name, p.Station.code]);
  dataSh.getRange(2, startCol, output.length, 2).clearContent().setValues(output);

  // ドロップダウン候補に駅名をセット
  const rule = SpreadsheetApp.newDataValidation()
    .requireValueInList(names, true) // リストから選べる
    .setAllowInvalid(false) // 無効入力を禁止
    .build();
  e.range.clearDataValidations();
  e.range.setDataValidation(rule);
}

駅名を取得するのは fetchStations 関数になります。 API_KEY は自分のAPIキーで、ファイルの一番上などで定義しておきます。

// Ekispert API キー(実際に使用する際はご自身のキーに差し替えてください)
const API_KEY = 'YOUR_API_KEY';
/**
 * 駅検索用APIを呼び出して駅名とコードの配列を取得
 * @param {string} name - 駅名またはキーワード
 * @returns {Array} - {Station:{Name,code}} の配列
 */
function fetchStations(name) {
  const url = `https://api.ekispert.jp/v1/json/station/light?key=${API_KEY}` +
    `&name=${encodeURIComponent(name)}&nameMatchType=partial&type=train`;
  const res = UrlFetchApp.fetch(url); // API呼び出し
  if (res.getResponseCode() !== 200) return []; // エラー時は空配列
  console.log(res.getContentText()); // ログに生JSONを出力(開発時用)
  const points = JSON.parse(res.getContentText()).ResultSet.Point;
  return Array.isArray(points) ? points : [points]; // 配列化して返す
}

APIのレスポンスについては駅簡易情報 - 駅すぱあと API Documents 駅データ・経路検索のWebAPIを参照してください。

image2.png

経路探索機能の実装

続いて経路探索です。ここではメニューで選択されたタイミングで、 runRoutes 関数が呼ばれます。まず、メニューに追加します。

// スプレッドシートを開いたときに自動実行される関数
function onOpen() {
  SpreadsheetApp.getUi() // UI操作用のオブジェクト取得
    .createMenu('Ekispert') // メニュー名を作成
    .addItem('ルート探索', 'runRoutes') // メニュー項目と実行関数を指定
    .addToUi(); // シートのUIに追加
}

image1.png

runRoutes 関数の実装です。選択されている駅名から、データシートにあるコードに変換して経路探索を実行しています。

/**
 * 入力された駅名リストをもとに経路検索を実行し、新しいシートに結果を出力
 */
function runRoutes() {
  const ss = SpreadsheetApp.getActive();
  const input = ss.getSheetByName('経路入力'); // 入力用シート
  const codes = []; // 駅コード一覧
  const names = []; // 駅名一覧

  // 出発、経由1、経由2、到着の4行分をループ
  for (let i = 1; i <= 4; i++) {
    const cell = input.getRange(i, 2).getValue(); // B列の値を取得
    // 出発(1行目)・到着(4行目)が未入力ならアラートして中断
    if (!cell && (i === 1 || i === 4)) {
      SpreadsheetApp.getUi().alert('出発駅/到着駅は必須です');
      return;
    }
    if (cell) {
      names.push(cell); // 駅名を格納
      const data = ss.getSheetByName('データ'); // コード管理用シート
      const code = data.createTextFinder(cell).findNext(); // 駅名を検索

      if (!code) {
        SpreadsheetApp.getUi().alert(`${i}行目の名前が見つかりません(${cell})`);
        return;
      }
      // 検索結果の隣のセルにある駅コードを取得
      codes.push(data.getRange(code.getRow(), code.getColumn() + 1).getValue());
    }
  }

  // コードから経路APIを呼び出し
  const courses = fetchRoutes(codes.join(':'));
  const base = `${names[0]}から${names[names.length - 1]}`; // シート名のベース
  const outSheet = addSheetWithIncrement(base); // 新規シート

  let col = 1;
  courses.forEach((course, idx) => {
    // ルートごとにヘッダーを書き込み
    outSheet.getRange(1, col).setValue(`ルート${idx + 1}`);
    const price = course.Price.find(p => p.kind === 'FareSummary')?.Oneway || 0;
    outSheet.getRange(2, col).setValue(`合計:${price}円`);

    // Route が単体or配列の可能性に対応
    let routes = course.Route;
    if (!Array.isArray(routes)) routes = [routes];

    // データ開始行とヘッダー
    let row = 4;
    outSheet.getRange(row, col, 1, 3).setValues([['乗車駅', '路線', '降車駅']]);
    row++;

    // 各区間ごとに駅名&路線名を書き込む
    routes.forEach(route => {
      let pts = route.Point;
      if (!Array.isArray(pts)) pts = [pts];

      let lines = route.Line;
      if (!Array.isArray(lines)) lines = [lines];

      for (let i = 0; i < lines.length; i++) {
        const start = pts[i].Station.Name;
        const end = pts[i + 1].Station.Name;
        const lineName = lines[i].Name;
        outSheet.getRange(row, col, 1, 3).setValues([[start, lineName, end]]);
        row++;
      }
    });

    col += 4; // 次のルートは4列右に出力
  });
}

経路探索を行っているのは fetchRoutes 関数です。

/**
 * 経路探索APIを呼び出して経路一覧を取得
 * @param {string} viaList - コロン区切りの駅コード(例: code1:code2...)
 * @returns {Array} - Courseオブジェクトの配列(ルート情報)
 */
function fetchRoutes(viaList) {
  const url = `https://api.ekispert.jp/v1/json/search/course/extreme` +
    `?key=${API_KEY}&viaList=${viaList}&searchType=plain`;
  const res = UrlFetchApp.fetch(url);
  if (res.getResponseCode() !== 200) return [];
  let courses = JSON.parse(res.getContentText()).ResultSet.Course;
  return Array.isArray(courses) ? courses : [courses];
}

繰り返し実行するとシート名が被ってしまうので、ユニークな名前(末尾に数字を追加)にしています。 addSheetWithIncrement にてシート名を作成しています。

/**
 * 新しいシートを追加する際に名前が被らないようにするラッパー
 * @param {string} baseName - 希望のシート名
 * @returns {Sheet} - 作成されたシートオブジェクト
 */
function addSheetWithIncrement(baseName) {
  const ss = SpreadsheetApp.getActive();
  const uniqueName = getUniqueSheetName(baseName); // 重複を避けた名前を取得
  return ss.insertSheet(uniqueName); // 新規シートを作成
}

/**
 * シート名が重複している場合、末尾に (2), (3)… をつけて一意な名前を作成する
 * @param {string} baseName - 元になるシート名
 * @returns {string} - 一意なシート名
 */
function getUniqueSheetName(baseName) {
  const ss = SpreadsheetApp.getActive(); // 現在のスプレッドシート取得
  let name = baseName;
  let counter = 1;

  // 同名シートが存在する限り、番号を振って検証
  while (ss.getSheetByName(name)) {
    counter++;
    name = `${baseName} (${counter})`;
  }
  return name; // 重複しないユニーク名を返す
}

結果を出力する際には、結果を横に並べる形で出力しています。その際、列番号を AB といった列名文字列に変換するのが columnToLetter 関数になります。

/**
 * 列番号を A, B, …, AA … の列名文字列に変換する関数
 * @param {number} column - 列番号 (1スタート)
 * @returns {string} - 列名文字列
 */
function columnToLetter(column) {
  let temp, letter = '';
  while (column > 0) {
    temp = (column - 1) % 26;
    letter = String.fromCharCode(65 + temp) + letter;
    column = Math.floor((column - temp - 1) / 26);
  }
  return letter;
}

getPrice 関数は、運賃情報を取得・返却します。今回は通常運賃のみですが、他にも定期(1〜12ヶ月)の運賃情報なども取得できます。新幹線などを利用した場合は、取得が異なるので注意してください(詳しくは経路探索 - 駅すぱあと API Documents 駅データ・経路検索のWebAPIにて確認してください)。

/**
 * Price配列から「合計運賃(FareSummary)」を抽出して返す
 * @param {Array} prices - Priceオブジェクトの配列
 * @returns {number} - 運賃(円)
 */
function getPrice(prices) {
  return prices.filter(p => p.kind === 'FareSummary')[0].Oneway || 0;
}

コードについて

今回のコードはgoofmint/ekispart_routes_gas_demoにアップロードしてあります。実装時の参考にしてください。

まとめ

今回は「駅すぱあと API」を使って、Google Apps Scriptから経路探索および運賃の算出を試しました。経路探索の他にも、「駅すぱあと API」には様々な機能があります。ぜひ、自分のアプリケーションや基幹システムに組み込んで、業務効率化を実現してください。

駅すぱあと API Documents 駅データ・経路検索のWebAPI

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?