Last active
June 18, 2025 16:13
-
-
Save satakagi/927d5e5743bad89d821048684d4b8957 to your computer and use it in GitHub Desktop.
ジオコーダと、度分秒変換機能 for Google SpreadSheet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| #スクリプトの利用方法 | |
| * Google スプレッドシートを開く: 緯度経度データを含む Google スプレッドシートを開きます。 | |
| * Apps Script エディタを開く: メニューバーから「拡張機能」>「Apps Script」を選択します。 | |
| * コードを貼り付ける: 開いた Apps Script エディタに上記のコードをコピーして貼り付けます。 | |
| * 保存する: フロッピーディスクのアイコンをクリックするか、Ctrl + S (Windows) / Cmd + S (Mac) でスクリプトを保存します。 | |
| #実行する: | |
| * Google スプレッドシートのタブを一度閉じ、再度開きます。または、ブラウザのページを再読み込み (F5キーなど) します。 | |
| * スプレッドシートが再読み込みされると、メニューバーに「住所変換」という新しいメニューが表示され、プルダウンに各機能がリストされていますので選んで実行します。 | |
| **/ | |
| // スクリプト停止フラグのキー | |
| const STOP_FLAG_KEY = 'stopScriptFlag'; | |
| /** | |
| * CSISシンプルジオコーディング実験サービスを使って、 | |
| * スプレッドシートの住所カラムおよび緯度・経度カラムを柔軟に特定し、 | |
| * 必要に応じて出力カラムを自動で追加して変換結果を書き込みます。 | |
| * 複数のリクエストを UrlFetchApp.fetchAll() で並列実行し、 | |
| * 進捗状況を住所ヘッダーセルに表示します。 | |
| * | |
| * - 10件処理ごとにシートに結果を書き込みます。 | |
| * - 既に緯度経度が入力済みのレコードはスキップします。 | |
| * - 途中でスクリプトを停止する機能を追加しました。 | |
| */ | |
| function convertAddressesWithCSIS_SmartFeatures() { | |
| const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); | |
| const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; | |
| let addressColumnIndex = -1; | |
| let latColumnIndex = -1; | |
| let lngColumnIndex = -1; | |
| // === カラムの特定 === | |
| for (let i = 0; i < headers.length; i++) { | |
| const header = String(headers[i]).toLowerCase().trim(); | |
| if (header === '住所' || header === 'address' || header === 'アドレス') { | |
| addressColumnIndex = i; | |
| } else if (header === '緯度' || header === 'latitude') { | |
| latColumnIndex = i; | |
| } else if (header === '経度' || header === 'longitude') { | |
| lngColumnIndex = i; | |
| } | |
| } | |
| if (addressColumnIndex === -1) { | |
| Browser.msgBox("エラー", "「住所」「address」「アドレス」のいずれかのヘッダーが見つかりませんでした。処理を終了します。", Browser.Buttons.OK); | |
| return; | |
| } | |
| const addressHeaderCell = sheet.getRange(1, addressColumnIndex + 1); | |
| const originalAddressHeader = addressHeaderCell.getValue(); | |
| let currentLastColumn = sheet.getLastColumn(); | |
| if (latColumnIndex === -1) { | |
| currentLastColumn++; | |
| sheet.getRange(1, currentLastColumn).setValue("緯度"); | |
| latColumnIndex = currentLastColumn - 1; | |
| Logger.log("「緯度」カラムが見つからなかったため、列 " + currentLastColumn + " に追加しました。"); | |
| } | |
| if (lngColumnIndex === -1) { | |
| currentLastColumn++; | |
| sheet.getRange(1, currentLastColumn).setValue("経度"); | |
| lngColumnIndex = currentLastColumn - 1; | |
| Logger.log("「経度」カラムが見つからなかったため、列 " + currentLastColumn + " に追加しました。"); | |
| } | |
| const lastRow = sheet.getLastRow(); | |
| const BASE_URL = "https://geocode.csis.u-tokyo.ac.jp/cgi-bin/simple_geocode.cgi"; | |
| const BATCH_SIZE = 10; // 一度に処理するリクエスト数 (ご要望に合わせて10に変更) | |
| // === スキップ済みレコードの考慮とリクエストの準備 === | |
| // 全データを一度に取得し、スキップ対象のレコードをフィルタリング | |
| // getLastColumn() で取得できる最終列まで全データ読み込むことで、緯度経度カラムが追加された後でも対応できる | |
| const allData = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues(); | |
| let recordsToProcess = []; // 処理すべきレコードの情報を格納 | |
| for (let i = 0; i < allData.length; i++) { | |
| const rowData = allData[i]; | |
| const address = String(rowData[addressColumnIndex]).trim(); | |
| const existingLat = rowData[latColumnIndex]; | |
| const existingLng = rowData[lngColumnIndex]; | |
| const sheetRowIndex = i + 2; // スプレッドシートの行番号 (1行目ヘッダー、2行目からデータ開始) | |
| // 住所があり、かつ緯度経度カラムが空である(または数値でない)場合に処理対象とする | |
| if (address && (typeof existingLat !== 'number' || typeof existingLng !== 'number')) { | |
| recordsToProcess.push({ | |
| sheetRow: sheetRowIndex, | |
| address: address | |
| }); | |
| } | |
| } | |
| // === スクリプト停止フラグのリセット === | |
| // 処理開始時に停止フラグをクリア | |
| PropertiesService.getUserProperties().deleteProperty(STOP_FLAG_KEY); | |
| const totalRecordsToProcess = recordsToProcess.length; | |
| let processedCount = 0; // スキップしたものは含まない、実際にジオコーディングを試みた件数 | |
| // === 進捗の初期表示 === | |
| addressHeaderCell.setValue(`${originalAddressHeader} 処理中... 0/${totalRecordsToProcess}`); | |
| SpreadsheetApp.flush(); | |
| // === バッチに分けてリクエストを並列実行 === | |
| for (let i = 0; i < totalRecordsToProcess; i += BATCH_SIZE) { | |
| // === 停止フラグのチェック === | |
| // 各バッチ処理の開始前にフラグを確認 | |
| if (PropertiesService.getUserProperties().getProperty(STOP_FLAG_KEY) === 'true') { | |
| Browser.msgBox("処理中断", "スクリプトが中断されました。", Browser.Buttons.OK); | |
| Logger.log("スクリプトが停止フラグにより中断されました。"); | |
| break; // ループを中断 | |
| } | |
| const batchRecords = recordsToProcess.slice(i, i + BATCH_SIZE); | |
| const batchRequests = batchRecords.map(record => { | |
| const encodedAddress = encodeURIComponent(record.address); | |
| const url = `${BASE_URL}?charset=UTF8&addr=${encodedAddress}`; | |
| return { url: url, muteHttpExceptions: true }; | |
| }); | |
| let batchResultsToWrite = []; // このバッチで書き込む結果を一時的に保存 | |
| try { | |
| const responses = UrlFetchApp.fetchAll(batchRequests); | |
| for (let j = 0; j < responses.length; j++) { | |
| const response = responses[j]; | |
| const record = batchRecords[j]; // 元のレコード情報を取得 | |
| const targetSheetRow = record.sheetRow; // スプレッドシートの実際の行番号 | |
| const address = record.address; // 処理対象の住所 | |
| let lat = "変換失敗"; | |
| let lng = "変換失敗"; | |
| let logMessage = `行: ${targetSheetRow}, 住所: ${address}, `; | |
| if (response.getResponseCode() === 200) { | |
| try { | |
| const xml = response.getContentText(); | |
| const root = XmlService.parse(xml).getRootElement(); | |
| const candidate = root.getChild('candidate'); | |
| if (candidate) { | |
| const latitude = candidate.getChildText('latitude'); | |
| const longitude = candidate.getChildText('longitude'); | |
| if (latitude && longitude) { | |
| lat = parseFloat(latitude); | |
| lng = parseFloat(longitude); | |
| logMessage += `緯度: ${lat}, 経度: ${lng}`; | |
| } else { | |
| logMessage += `ジオコーディング結果なし。レスポンス: ${xml}`; | |
| } | |
| } else { | |
| logMessage += `candidate要素なし。レスポンス: ${xml}`; | |
| } | |
| } catch (parseError) { | |
| logMessage += `XMLパースエラー: ${parseError.message}, レスポンス: ${response.getContentText()}`; | |
| } | |
| } else { | |
| logMessage += `HTTPエラー: ${response.getResponseCode()}, ${response.getContentText()}`; | |
| } | |
| Logger.log(logMessage); | |
| batchResultsToWrite.push({ row: targetSheetRow, lat: lat, lng: lng }); | |
| } | |
| } catch (e) { | |
| Logger.log(`fetchAll実行中にエラーが発生しました: ${e.message}`); | |
| batchRecords.forEach(record => { | |
| batchResultsToWrite.push({ row: record.sheetRow, lat: "バッチエラー", lng: "バッチエラー" }); | |
| }); | |
| } | |
| // === バッチごとの結果書き込み === | |
| batchResultsToWrite.forEach(result => { | |
| sheet.getRange(result.row, latColumnIndex + 1).setValue(result.lat); | |
| sheet.getRange(result.row, lngColumnIndex + 1).setValue(result.lng); | |
| }); | |
| processedCount += batchResultsToWrite.length; // 実際に書き込んだ数をカウント | |
| // === 進捗の更新 === | |
| addressHeaderCell.setValue(`${originalAddressHeader} 処理中... ${processedCount}/${totalRecordsToProcess}`); | |
| SpreadsheetApp.flush(); | |
| // CSISのサーバーへの負荷とGASの実行時間制限を考慮し、バッチ間に少し間隔を設ける | |
| if (i + BATCH_SIZE < totalRecordsToProcess) { | |
| Utilities.sleep(1000); | |
| } | |
| } | |
| // === 最終的な進捗表示とヘッダー復元 === | |
| addressHeaderCell.setValue(originalAddressHeader); // 元の項目名に戻す | |
| SpreadsheetApp.flush(); | |
| if (PropertiesService.getUserProperties().getProperty(STOP_FLAG_KEY) === 'true') { | |
| Browser.msgBox("処理完了", "スクリプトが中断され、処理が完了しました。", Browser.Buttons.OK); | |
| } else { | |
| Browser.msgBox("処理完了", "住所の緯度経度変換がすべて完了しました!", Browser.Buttons.OK); | |
| } | |
| } | |
| /** | |
| * 実行中のジオコーディングスクリプトを停止するフラグを設定します。 | |
| * スクリプトは次のバッチ処理の前に停止します。 | |
| */ | |
| function stopGeocodingScript() { | |
| PropertiesService.getUserProperties().setProperty(STOP_FLAG_KEY, 'true'); | |
| Browser.msgBox("停止リクエスト", "スクリプトに停止リクエストが送られました。現在のバッチ処理が完了次第、停止します。", Browser.Buttons.OK); | |
| } | |
| /** | |
| * スプレッドシートにカスタムメニューを追加します。 | |
| */ | |
| function onOpen() { | |
| const ui = SpreadsheetApp.getUi(); | |
| ui.createMenu('住所変換') | |
| .addItem('CSISで緯度経度を一括変換 (スマート機能)', 'convertAddressesWithCSIS_SmartFeatures') | |
| .addSeparator() // 区切り線を追加 | |
| .addItem('変換を中断する', 'stopGeocodingScript') | |
| .addToUi(); | |
| } | |
| /** | |
| * スプレッドシートの緯度経度データを度分秒 (DMS) または度分 (DM) 形式から | |
| * 小数点以下の度数 (DD) 形式に変換し、新しい列を追加して書き込みます。 | |
| * ヘッダーのカッコ内のフォーマット指定に基づいて変換を行います。 | |
| */ | |
| function convertLatLonToDecimalAppendColumns() { | |
| const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); | |
| const dataRange = sheet.getDataRange(); | |
| const values = dataRange.getValues(); | |
| if (values.length < 1) { | |
| SpreadsheetApp.getUi().alert('スプレッドシートにデータがありません。'); | |
| return; | |
| } | |
| const headerRow = values[0]; | |
| const newHeader = [...headerRow]; // ヘッダーをコピー | |
| let latColIndex = -1; | |
| let lonColIndex = -1; | |
| let latFormat = ''; | |
| let lonFormat = ''; | |
| // ヘッダーを解析して緯度経度カラムとフォーマットを特定 | |
| for (let i = 0; i < headerRow.length; i++) { | |
| const header = String(headerRow[i]).toLowerCase(); // 値が数値の場合があるのでStringに変換 | |
| const match = header.match(/(緯度|latitude|経度|longitude)\s*\((.*)\)/); | |
| if (match) { | |
| const type = match[1]; | |
| const format = match[2]; | |
| if (type === '緯度' || type === 'latitude') { | |
| latColIndex = i; | |
| latFormat = format; | |
| } else if (type === '経度' || type === 'longitude') { | |
| lonColIndex = i; | |
| lonFormat = format; | |
| } | |
| } | |
| } | |
| if (latColIndex === -1 && lonColIndex === -1) { | |
| SpreadsheetApp.getUi().alert('緯度または経度を示すヘッダーが見つかりませんでした。ヘッダーの例: 緯度(dd:mm:ss.s), 経度(d:m)。'); | |
| return; | |
| } | |
| const outputData = []; // 変換後のデータ格納用 | |
| let currentColumnCount = headerRow.length; // 現在の列数 | |
| // 新しいヘッダー列を追加 | |
| let newLatColAdded = false; | |
| let newLonColAdded = false; | |
| if (latColIndex !== -1) { | |
| newHeader.push('変換済み緯度 (DD)'); | |
| newLatColAdded = true; | |
| currentColumnCount++; | |
| } | |
| if (lonColIndex !== -1) { | |
| newHeader.push('変換済み経度 (DD)'); | |
| newLonColAdded = true; | |
| currentColumnCount++; | |
| } | |
| outputData.push(newHeader); // 新しいヘッダーをoutputDataに追加 | |
| // データ行を処理 | |
| for (let i = 1; i < values.length; i++) { | |
| const row = values[i]; | |
| const newRow = [...row]; // 元の行のデータをコピー | |
| let convertedLat = ''; | |
| let convertedLon = ''; | |
| if (latColIndex !== -1) { | |
| const latValue = row[latColIndex]; | |
| convertedLat = convertToDecimal(latValue, latFormat); | |
| } | |
| if (lonColIndex !== -1) { | |
| const lonValue = row[lonColIndex]; | |
| convertedLon = convertToDecimal(lonValue, lonFormat); | |
| } | |
| // 変換結果を新しい行に追加 | |
| if (newLatColAdded) { | |
| newRow.push(convertedLat); | |
| } | |
| if (newLonColAdded) { | |
| newRow.push(convertedLon); | |
| } | |
| // 行の長さを揃えるために、不足しているセルを空文字で埋める | |
| while (newRow.length < currentColumnCount) { | |
| newRow.push(''); | |
| } | |
| outputData.push(newRow); | |
| } | |
| // 新しいデータをシートに書き戻す | |
| // シートの全範囲を新しいデータで上書きすることで、既存の列を保持しつつ新しい列を追加する | |
| // これにより、元のデータはそのまま残り、追加の列に変換結果が書き込まれる | |
| sheet.getRange(1, 1, outputData.length, outputData[0].length).setValues(outputData); | |
| SpreadsheetApp.getUi().alert('緯度経度変換が完了し、新しい列に追加されました。'); | |
| } | |
| /** | |
| * 度分秒または度分形式の文字列を小数点以下の度数に変換します。 | |
| * 固定長フォーマット (例: ddmmss.sss) にも対応し、小数点以下は可変長として扱います。 | |
| * @param {string} value 変換する緯度経度文字列。 | |
| * @param {string} format フォーマット文字列 (例: "dd:mm:ss.s", "d:m", "ddmmss.sss")。 | |
| * @returns {number|string} 変換された小数点以下の度数、またはエラーメッセージ。 | |
| */ | |
| function convertToDecimal(value, format) { | |
| if (typeof value !== 'string' || !value.trim()) { | |
| return 'データなし'; | |
| } | |
| const cleanValue = value.trim(); | |
| let degrees = 0; | |
| let minutes = 0; | |
| let seconds = 0; | |
| let sign = 1; | |
| // 方位を示す文字 (N, S, E, W) を考慮 | |
| const lastChar = cleanValue.slice(-1).toLowerCase(); | |
| if (lastChar === 's' || lastChar === 'w') { | |
| sign = -1; | |
| } | |
| const numericValue = cleanValue.replace(/[nsew]/i, ''); // 方位文字を除去 | |
| // フォーマットに区切り文字が含まれるかチェック | |
| const hasSeparators = /[^\w.]/.test(format); // 数字、文字、小数点以外の文字があれば区切り文字ありと判断 | |
| // フォーマットが固定長形式(d, m, s, およびオプションの小数点以下sのみで構成され、区切り文字なし)であるかを確認 | |
| // 小数点以下のsの桁数指定は考慮しない(可変長扱いのため) | |
| if (!hasSeparators && format.match(/^[dms]+(?:\.s*)?$/)) { // .s* は小数点以下のsが0個以上でもマッチ | |
| // --- 固定長フォーマットの解析 --- | |
| let tempValue = numericValue; // 解析中に切り取っていく文字列 | |
| // 度を抽出 | |
| const dMatch = format.match(/^d+/); | |
| const dCount = dMatch ? dMatch[0].length : 0; | |
| if (dCount > 0) { | |
| if (tempValue.length < dCount) return `解析エラー: 度数部分の桁数が不足しています ('${format}', データ: '${numericValue}')`; | |
| degrees = parseFloat(tempValue.substring(0, dCount)); | |
| tempValue = tempValue.substring(dCount); | |
| } else { | |
| // 度数の指定がない場合は、エラーとする (固定長は必ず度数から始まると仮定) | |
| return `フォーマットエラー: 固定長形式で度数(d)の指定がありません ('${format}')`; | |
| } | |
| // 分を抽出 | |
| const mMatch = format.match(/m+/); | |
| const mCount = mMatch ? mMatch[0].length : 0; | |
| if (mCount > 0) { | |
| if (tempValue.length < mCount) return `解析エラー: 分数部分の桁数が不足しています ('${format}', データ: '${numericValue}')`; | |
| minutes = parseFloat(tempValue.substring(0, mCount)); | |
| tempValue = tempValue.substring(mCount); | |
| } | |
| // 秒を抽出 | |
| // 秒は残りのすべてを秒とみなす(小数点以下は可変長) | |
| const sMatch = format.match(/s+/); // sの整数部分の桁数を取得 | |
| const sIntCount = sMatch ? sMatch[0].length : 0; | |
| if (sIntCount > 0) { | |
| if (tempValue.length === 0) return `解析エラー: 秒数部分のデータが不足しています ('${format}', データ: '${numericValue}')`; | |
| let secValueStr; | |
| const dotIndex = tempValue.indexOf('.'); | |
| if (dotIndex !== -1) { | |
| // 小数点がある場合、整数部と小数部を分けて考慮 | |
| const intPart = tempValue.substring(0, dotIndex); | |
| const decPart = tempValue.substring(dotIndex + 1); | |
| if (intPart.length < sIntCount) { | |
| // 秒の整数部分の桁数がフォーマットより少ない場合、0で埋める | |
| secValueStr = intPart.padStart(sIntCount, '0') + '.' + decPart; | |
| } else { | |
| // 秒の整数部分の桁数がフォーマット以上の場合、フォーマットに合わせた整数部を使い、残りを小数部として扱う | |
| secValueStr = intPart.substring(0, sIntCount) + '.' + intPart.substring(sIntCount) + decPart; | |
| } | |
| } else { | |
| // 小数点がない場合、指定された秒の整数桁数で区切り、残りを小数点以下とみなす | |
| if (tempValue.length > sIntCount) { | |
| secValueStr = tempValue.substring(0, sIntCount) + '.' + tempValue.substring(sIntCount); | |
| } else { | |
| secValueStr = tempValue; // 秒の整数部分のみ、または桁数不足 | |
| } | |
| } | |
| seconds = parseFloat(secValueStr); | |
| } else { | |
| // 秒の指定がないがデータが残っている場合 (例: ddmmでデータが342411.123) | |
| if (tempValue.length > 0 && parseFloat(tempValue) !== 0) { | |
| return `フォーマットエラー: フォーマットに秒数(s)の指定がありませんが、データに秒数部分が含まれています ('${format}', データ: '${numericValue}')`; | |
| } | |
| } | |
| if (isNaN(degrees) || isNaN(minutes) || isNaN(seconds)) { | |
| return `解析エラー: 固定長形式の数値変換に失敗しました ('${numericValue}')`; | |
| } | |
| return sign * (degrees + (minutes / 60) + (seconds / 3600)); | |
| } else if (hasSeparators) { | |
| // --- 区切り文字ありフォーマットの解析 (既存ロジック) --- | |
| const formatParts = format.split(/[^dms.]+/).filter(part => part !== ''); | |
| const separatorsArray = format.split(/[dms.]+/).filter(part => part !== ''); | |
| if (formatParts.length === 0) { | |
| return 'フォーマットエラー: フォーマット情報が不正です。'; | |
| } | |
| let parts; | |
| if (separatorsArray.length > 0) { | |
| const regexSeparator = new RegExp(separatorsArray[0].replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'); | |
| parts = numericValue.split(regexSeparator); | |
| } else { | |
| parts = [numericValue]; | |
| } | |
| if (parts.length === 1 && formatParts.length > 1 && !formatParts.some(p => p.includes('d.'))) { | |
| return `フォーマットエラー: 区切り文字がなく、'${format}'に対応する解析ができません。`; | |
| } | |
| for (let i = 0; i < parts.length; i++) { | |
| const part = parseFloat(parts[i]); | |
| if (isNaN(part)) { | |
| return `解析エラー: 数値に変換できない値 '${parts[i]}' が含まれています。`; | |
| } | |
| if (i === 0) { | |
| degrees = part; | |
| } else if (i === 1) { | |
| minutes = part; | |
| } else if (i === 2) { | |
| seconds = part; | |
| } | |
| } | |
| if (formatParts.some(p => p.includes('d.')) && formatParts.length === 1) { | |
| // 小数点以下の度数形式 (例: dd.d) | |
| return sign * parseFloat(numericValue); | |
| } else { | |
| // 度分秒または度分形式 | |
| return sign * (degrees + (minutes / 60) + (seconds / 3600)); | |
| } | |
| } else { | |
| return 'フォーマットエラー: 未知のフォーマット形式です。区切り文字がない場合はd,m,sの桁数を正確に指定してください。'; | |
| } | |
| } | |
| /** | |
| * スプレッドシートにカスタムメニューを追加します。 | |
| */ | |
| function onOpen() { | |
| const ui = SpreadsheetApp.getUi(); | |
| ui.createMenu('住所変換') | |
| .addItem('CSISで緯度経度を一括変換 (スマート機能)', 'convertAddressesWithCSIS_SmartFeatures') | |
| .addItem('度分秒から度への変換', 'convertLatLonToDecimalAppendColumns') | |
| .addSeparator() | |
| .addItem('変換を中断する', 'stopGeocodingScript') | |
| .addSeparator() | |
| .addItem('使い方とヘッダー書式', 'showHelpSheet') // ★この行を追加または変更 | |
| .addToUi(); | |
| } | |
| /** | |
| * ヘッダーの書き方を含む説明シートをスプレッドシートに追加します。 | |
| * 既にシートが存在する場合は、そこに移動します。 | |
| */ | |
| function showHelpSheet() { | |
| const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); | |
| const sheetName = '住所変換_使い方と書式'; // シート名を明確化 | |
| let helpSheet = spreadsheet.getSheetByName(sheetName); | |
| if (!helpSheet) { | |
| helpSheet = spreadsheet.insertSheet(sheetName); | |
| helpSheet.clear(); // 既存のコンテンツをクリア | |
| // 説明テキストを配列で用意 | |
| const helpContent = [ | |
| "## 緯度経度変換・ジオコーディング機能 - 使い方とヘッダー書式", | |
| "", | |
| "このシートでは、このスプレッドシートに追加された住所変換スクリプトの各機能を利用するためのヘッダーの記述方法を説明します。", | |
| "変換したいカラムの**1行目(ヘッダー行)**に、以下のルールに従ってフォーマットを記述してください。", | |
| "", | |
| "---", | |
| "### 1. 住所から緯度経度への変換 (CSISジオコーディング)", | |
| "住所から緯度と経度を取得する機能です。", | |
| "", | |
| "#### 必要なヘッダー", | |
| "- `住所` または `address` または `アドレス`", | |
| "- 緯度と経度の出力先カラムは、ヘッダーがなければスクリプトが自動で `緯度` と `経度` という名前で新しい列を追加します。", | |
| "", | |
| "#### 例", | |
| "| 住所 | 緯度 | 経度 | (その他のカラム) |", | |
| "| :---------- | :------- | :------- | :--------------- |", | |
| "| 東京都庁 | (自動入力)| (自動入力)| ... |", | |
| "| 大阪府庁 | | | ... |", | |
| "", | |
| "#### 注意点", | |
| "- 既に緯度経度が入力済みの行はスキップされます。", | |
| "- 処理中に「住所変換」メニューから「変換を中断する」を選択すると、スクリプトを停止できます。", | |
| "", | |
| "---", | |
| "### 2. 度分秒(DMS)・度分(DM)形式から小数点以下の度数(DD)形式への変換", | |
| "DMS/DM形式の緯度経度データを、一般的な小数点以下の度数(Decimal Degrees: DD)形式に変換する機能です。", | |
| "", | |
| "#### 必要なヘッダー", | |
| "変換したい緯度または経度のカラムのヘッダーに、以下に示す**フォーマットを丸括弧 `()` で囲んで記述**してください。", | |
| "キーワードは `緯度` または `Latitude`、`経度` または `Longitude` を使用してください。", | |
| "", | |
| "#### フォーマット記述のルール", | |
| "- `d`, `m`, `s` はそれぞれ「度」「分」「秒」の数値が入ることを示します。", | |
| "- `.` (ピリオド)は小数点を示します。`.s` や `.sss` のように続く `s` の数は、**小数点以下の桁数の目安**となりますが、**実際のデータでは可変長として読み込まれます**。", | |
| "- `:` や ` `(スペース)、`-` などは区切り文字として認識されます。", | |
| "", | |
| "#### フォーマットとデータ例", | |
| "| ヘッダーの記述例 | 説明 | データ例 | 変換結果の例 (DD形式) |", | |
| "| :-------------------- | :--------------------------------- | :---------------- | :------------------------ |", | |
| "| `緯度(d:m:s.s)` | 度:分:秒.小数点秒(区切り文字あり) | `35:41:39.123N` | `35.69420083` |", | |
| "| `経度(d m s)` | 度 分 秒(スペース区切り) | `139 41 39E` | `139.69416667` |", | |
| "| `緯度(d:m.m)` | 度:分.小数点分 | `35:41.5S` | `-35.69166667` |", | |
| "| `経度(dd.d)` | 度(小数点数) | `139.750000` | `139.750000` |", | |
| "| `緯度(ddmmss.sss)` | 度分秒(固定長、小数点以下可変長) | `354139.123` | `35.69420083` |", | |
| "| `経度(dddmmss.s)` | 度分秒(固定長、小数点以下可変長) | `1240322.223` | `124.05617306` |", | |
| "", | |
| "#### 方位を示す文字について", | |
| "データの末尾に `N` (北緯), `S` (南緯), `E` (東経), `W` (西経) を付けることで、正負を自動判別します。", | |
| "例: `35:41:39S` は `-35.6942` に変換されます。", | |
| "", | |
| "---", | |
| "### 共通の注意点", | |
| "- 変換対象のデータが入っているセルの**書式を「書式なしテキスト」**に設定することを強く推奨します。これにより、データが日付や数値として誤って解釈されることを防ぎます。", | |
| "- 緯度と経度のデータは、それぞれ別のセルに入力してください。", | |
| "- データに不明な文字が含まれている場合や、フォーマットと大きく異なる場合は「解析エラー」となります。", | |
| "- 元のデータが空の場合、変換後のカラムには「データなし」と表示されます。" | |
| ]; | |
| let rowNum = 1; | |
| let inTable = false; // テーブル内かどうかを管理するフラグ | |
| let tableColCount = 0; // 現在のテーブルの列数 | |
| for (const line of helpContent) { | |
| // 空行の処理 | |
| if (line.trim() === "") { | |
| rowNum++; | |
| continue; | |
| } | |
| const range = helpSheet.getRange(rowNum, 1); // デフォルトの書き込み開始セル | |
| if (line.startsWith("## ")) { | |
| range.setValue(line.substring(3)).setFontWeight("bold").setFontSize(16); | |
| inTable = false; | |
| } else if (line.startsWith("### ")) { | |
| range.setValue(line.substring(4)).setFontWeight("bold").setFontSize(14); | |
| inTable = false; | |
| } else if (line.startsWith("#### ")) { | |
| range.setValue(line.substring(5)).setFontWeight("bold").setFontSize(12); | |
| inTable = false; | |
| } else if (line.startsWith("---")) { // 水平線 | |
| // テーブルの区切り線と水平線を区別 | |
| if (inTable) { // テーブルのヘッダーとデータの間の区切り線 | |
| // 何もしない(テーブルの描画時に罫線で表現するため) | |
| } else { // 通常の水平線 | |
| // 罫線を引く際に、helpSheetオブジェクトからgetMaxColumnsを呼び出す | |
| const currentMaxCols = helpSheet.getMaxColumns(); // <-- ここを修正 | |
| if (currentMaxCols > 0) { | |
| helpSheet.getRange(rowNum, 1, 1, currentMaxCols).setBorder(true, true, true, true, false, false, "black", SpreadsheetApp.BorderStyle.SOLID_MEDIUM); | |
| } else { // データがない場合はA1に罫線 | |
| helpSheet.getRange(rowNum, 1).setBorder(true, true, true, true, false, false, "black", SpreadsheetApp.BorderStyle.SOLID_MEDIUM); | |
| } | |
| } | |
| inTable = false; // 水平線の後はテーブル終了 | |
| } else if (line.startsWith("|")) { // Markdownテーブル行 | |
| const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell !== ''); | |
| if (cells.length > 0) { | |
| // テーブルのヘッダー行の場合(`:`が含まれる区切り線がない行) | |
| if (!inTable) { | |
| tableColCount = cells.length; // テーブルの列数を取得 | |
| for (let col = 0; col < cells.length; col++) { | |
| helpSheet.getRange(rowNum, col + 1).setValue(cells[col]).setFontWeight("bold"); | |
| } | |
| // ヘッダーの下に罫線を引く | |
| helpSheet.getRange(rowNum, 1, 1, cells.length).setBorder(null, null, true, null, null, null, "black", SpreadsheetApp.BorderStyle.SOLID); | |
| inTable = true; | |
| } else { // テーブルのデータ行の場合 | |
| for (let col = 0; col < cells.length; col++) { | |
| helpSheet.getRange(rowNum, col + 1).setValue(cells[col]); | |
| } | |
| } | |
| } | |
| } else if (line.startsWith("- ")) { // リストアイテム | |
| range.setValue("・" + line.substring(2)); | |
| inTable = false; | |
| } else { // 通常のテキスト | |
| range.setValue(line); | |
| inTable = false; | |
| } | |
| rowNum++; | |
| } | |
| // 列幅の自動調整(テーブルの最大列数まで対応できるよう調整) | |
| helpSheet.autoResizeColumns(1, helpSheet.getMaxColumns()); // <-- ここも修正 | |
| SpreadsheetApp.getUi().alert('使い方シートを作成しました。'); | |
| } else { | |
| SpreadsheetApp.getUi().alert('「' + sheetName + '」シートに移動します。'); | |
| } | |
| spreadsheet.setActiveSheet(helpSheet); // シートをアクティブにする | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment