CSV/TSVでアクティブ列選択
概要
このマクロは、CSVファイル内でアクティブな列を選択するためのものです。現在のカーソル位置を基準に、対応する列を自動的に全て選択します。
フィールドのダブルクォーテーション囲み考慮に対応しており、ダブルクォーテーション内のデリミタ (カンマ、タブ記号) やエスケープされたダブルクォーテーション (2つ連なったダブルクォーテーション)、改行文字を含めて列選択を行うことができます。
注意事項
- Mery Ver 3.7.9 で動作確認しています。
使い方
- MeryでCSVファイルを開く
- 選択したい列にカーソルを移動させる
- このマクロを実行すると、カーソル位置に対応する列が全て選択される
ソースコード
#title = "CSV/TSV でアクティブ列選択"
BeginUndoGroup();
// フィールドのダブルクォーテーション囲みを考慮して列選択するかどうかのデフォルト値
// 考慮ありだと、ダブルクォーテーション内のデリミタや改行込みで列選択します。
// true: 考慮あり / false: 考慮なし
var CONSIDER_DOUBLE_QUOTE_DEFAULT = true;
// メニューID
var CSV_ID = 1;
var TSV_ID = 2;
var CONSIDER_DOUBLE_QUOTE_ID = 3;
// ダブルクォーテーションを考慮するかどうかの一時変更用
var considerDoubleQuote = CONSIDER_DOUBLE_QUOTE_DEFAULT;
var DELIMITER_LIST = {}
DELIMITER_LIST[CSV_ID] = ","
DELIMITER_LIST[TSV_ID] = "\t"
var console = {
log: function (str) {
outputBar.Writeln("---");
outputBar.Writeln(str);
}
};
outputBar.Clear();
var doc = document;
var sel = doc.selection;
main();
function main() {
// 現在の列位置を取得
var actPos = sel.GetActivePos();
while (true) {
// CSVかTSVかコンテキストメニューで指定
var menu = createContextMenu();
var menuTrack = menu.Track(0);
// メニュー選択がキャンセルされた場合は終了
if (menuTrack === 0) return;
// ダブルクォーテーションを考慮するかどうかの設定
if (menuTrack === CONSIDER_DOUBLE_QUOTE_ID) {
considerDoubleQuote = !considerDoubleQuote;
continue;
}
break;
}
var delimiter = "";
switch (menuTrack) {
case CSV_ID:
delimiter = DELIMITER_LIST[CSV_ID];
break;
case TSV_ID:
delimiter = DELIMITER_LIST[TSV_ID];
break;
}
if (delimiter === "") {
throw new Error("Delimiter is not set.");
}
var text = doc.Text;
var startEndRangeList = null;
if (considerDoubleQuote) {
// カーソル位置がダブルクォーテーション囲みかどうかチェック
var doubleQuoteStartPosition = getDoubleQuoteStartPosition(text, actPos, delimiter);
if (doubleQuoteStartPosition != -1) {
actPos = doubleQuoteStartPosition;
}
// カーソル位置のフィールド番号を取得
var cursorFieldCount = getCursorFieldNumber(text, actPos, delimiter);
// 各行のフィールド範囲を取得
startEndRangeList = getStartEndRangeList(text, cursorFieldCount, delimiter);
} else {
// カーソル位置のフィールド番号を取得
var cursorFieldCount = getCursorFieldNumberWithoutDoubleQuote(text, actPos, delimiter);
// 各行のフィールド範囲を取得
startEndRangeList = getStartEndRangeListWithoutDoubleQuote(text, cursorFieldCount, delimiter);
}
// 各行のフィールド範囲を選択
// HACK: カーソル位置のフィールド範囲を最後に選択するため、カーソル位置のフィールド範囲を一時的に保持
var rangeWithCursor = null;
for (var i = 0; i < startEndRangeList.length; i++) {
var startEnd = startEndRangeList[i];
if (startEnd.startPos <= actPos && actPos <= startEnd.endPos) {
rangeWithCursor = startEnd;
continue;
}
sel.AddPos(startEnd.startPos, startEnd.endPos);
}
if (rangeWithCursor != null) {
sel.AddPos(rangeWithCursor.startPos, rangeWithCursor.endPos);
}
}
function createContextMenu() {
const menu = CreatePopupMenu();
menu.Add("CSV でアクティブ列選択 (&C)", CSV_ID);
menu.Add("TSV でアクティブ列選択 (&T)", TSV_ID);
menu.Add("---", 0, meMenuSeparator);
var t = "ダブルクォーテーション囲み考慮";
if (considerDoubleQuote) {
menu.Add(t, CONSIDER_DOUBLE_QUOTE_ID, meMenuChecked);
} else {
menu.Add(t, CONSIDER_DOUBLE_QUOTE_ID);
}
return menu;
}
/**
* テキスト内の指定位置からダブルクォーテーションで囲まれた範囲の開始位置を取得します。
*
* @param {string} text - 対象のテキスト。
* @param {number} actPos - 現在の位置(0から始まるインデックス)。
* @param {string} delimiter - 区切り文字(カンマやタブなど)。
* @returns {number} ダブルクォーテーションで囲まれた範囲の開始位置。囲まれていない場合は -1 を返します。
*/
function getDoubleQuoteStartPosition(text, actPos, delimiter) {
var inDoubleQuote = false;
var fieldStartPos = -1;
for (var i = 0; i < actPos; i++) {
var checkString = text.charAt(i) + text.charAt(i + 1)
if (!inDoubleQuote && text.charAt(i) === '"' && (text.charAt(i - 1) === "\n" || i === 0)) {
inDoubleQuote = true;
fieldStartPos = i;
} else if (inDoubleQuote && checkString === '""') {
// ダブルクォーテーション内のダブルクォーテーション2つはエスケープされたダブルクォーテーションとして扱うので1文字スキップ
i++;
} else if (inDoubleQuote && (checkString === '"' + delimiter || checkString === '"\n')) {
// ダブルクォーテーション範囲から抜ける
if (i < actPos - 1) {
// HACK: カーソル位置が丁度ダブルクォーテーション範囲の終了位置の場合は無視することで、ダブルクォーテーション開始位置を返す
inDoubleQuote = false;
}
} else if (!inDoubleQuote && checkString === delimiter + '"') {
// ダブルクォーテーション範囲に入る
inDoubleQuote = true;
fieldStartPos = i + 1;
i++;
}
}
if (inDoubleQuote && fieldStartPos === -1) {
throw new Error("Invalid double quote range.");
}
if (inDoubleQuote) {
return fieldStartPos
}
return -1;
}
/**
* カーソル位置のフィールド番号を取得します。
*
* @param {string} text - テキスト全体の文字列。
* @param {number} actPos - カーソルの現在位置。
* @param {string} delimiter - フィールドを区切るデリミタ(例: カンマ、タブ)。
* @returns {number} カーソル位置のフィールド番号。
*/
function getCursorFieldNumber(text, actPos, delimiter) {
if (actPos == 0) {
return 1;
}
var inDoubleQuote = false;
var fieldCount = 1;
for (var i = actPos - 1; i >= 0; i--) {
var checkString = text.charAt(i - 1) + text.charAt(i)
if (inDoubleQuote && checkString === '""') {
// ダブルクォーテーション内のダブルクォーテーション2つはエスケープされたダブルクォーテーションとして扱うのでスキップ
i--;
} else if (inDoubleQuote && checkString === delimiter + '"') {
// ダブルクォーテーション範囲から抜ける
inDoubleQuote = false;
} else if (text.charAt(i) === delimiter) {
// ダブルクォーテーション範囲内のデリミタはカウントしない
if (!inDoubleQuote) {
fieldCount++;
}
// ダブルクォーテーション範囲に入る
if (checkString === '"' + delimiter) {
inDoubleQuote = true;
i--;
}
} else if (inDoubleQuote && text.charAt(i) === '"' && (text.charAt(i - 1) === '\n' || i === 0)) {
break;
} else if (!inDoubleQuote && text.charAt(i) === '\n') {
break;
}
}
return fieldCount;
}
/**
* カーソル位置のフィールド番号を取得します。(ダブルクォーテーションを考慮しない)
*
* @param {string} text - テキスト全体の文字列。
* @param {number} actPos - カーソルの現在位置。
* @param {string} delimiter - フィールドを区切るデリミタ(例: カンマ、タブ)。
* @returns {number} カーソル位置のフィールド番号。
*/
function getCursorFieldNumberWithoutDoubleQuote(text, actPos, delimiter) {
if (actPos == 0) {
return 1;
}
var fieldCount = 1;
for (var i = actPos - 1; i >= 0; i--) {
if (text.charAt(i) === delimiter) {
fieldCount++;
} else if (text.charAt(i) === '\n') {
break;
}
}
return fieldCount;
}
/**
* テキスト内の指定されたフィールドの開始位置と終了位置のリストを取得します。
*
* @param {string} text - 処理対象のテキスト。
* @param {number} cursorFieldCount - 開始位置と終了位置を取得するフィールドの番号(1から始まる)。
* @param {string} delimiter - フィールドを区切るデリミタ(例: カンマ、タブ)。
* @returns {Array<{startPos: number, endPos: number}>} 指定されたフィールドの開始位置と終了位置のオブジェクトのリスト。
*/
function getStartEndRangeList(text, cursorFieldCount, delimiter) {
var startEndRange = null;
var startEndRangeList = [];
var fieldCount = 1;
var inDoubleQuote = false;
var addStartEndRange = function (startEndRange) {
if (startEndRange.startPos != startEndRange.endPos) {
startEndRangeList.push(startEndRange);
}
}
// 選択範囲位置の追加はしたいが構文解析をスキップしたいときのフラグ
var skip = false;
for (var i = 0; i < text.length; i++) {
// 選択範囲の開始位置・終了位置を設定
if (startEndRange == null && fieldCount === cursorFieldCount) {
startEndRange = { startPos: i, endPos: i };
} else if (startEndRange != null && fieldCount === cursorFieldCount + 1) {
startEndRange.endPos = i - 1;
addStartEndRange(startEndRange);
startEndRange = null;
}
if (skip) {
skip = false;
continue;
}
// 構文解析
var checkString = text.charAt(i) + text.charAt(i + 1);
if (inDoubleQuote && checkString === '""') {
// ダブルクォーテーション内のダブルクォーテーション2つはエスケープされたダブルクォーテーションとして扱うので1文字スキップ
skip = true;
} else if (inDoubleQuote && checkString === '"' + delimiter) {
// ダブルクォーテーション範囲から抜ける
inDoubleQuote = false;
} else if (text.charAt(i) === delimiter) {
// ダブルクォーテーション範囲内のデリミタはカウントしない
if (!inDoubleQuote) {
fieldCount++;
}
// ダブルクォーテーション範囲に入る
if (!inDoubleQuote && checkString === delimiter + '"') {
inDoubleQuote = true;
skip = true;
}
} else if (inDoubleQuote && (checkString === '"\n' || checkString === '"')) {
inDoubleQuote = false;
} else if (!inDoubleQuote && text.charAt(i) === '"' && (text.charAt(i - 1) === '\n' || i == 0)) {
inDoubleQuote = true;
} else if (!inDoubleQuote && text.charAt(i) === '\n') {
fieldCount = 1;
// 改行でフィールドがリセットされるため、終了位置を設定
if (startEndRange != null) {
startEndRange.endPos = i;
addStartEndRange(startEndRange);
startEndRange = null;
}
} else if (i == text.length - 1 && startEndRange != null) {
// テキストの最後の場合
startEndRange.endPos = i + 1;
addStartEndRange(startEndRange);
}
}
return startEndRangeList;
}
/**
* テキスト内の指定されたフィールドの開始位置と終了位置のリストを取得します。(ダブルクォーテーションを考慮しない)
*
* @param {string} text - 処理対象のテキスト。
* @param {number} cursorFieldCount - 開始位置と終了位置を取得するフィールドの番号(1から始まる)。
* @param {string} delimiter - フィールドを区切るデリミタ(例: カンマ、タブ)。
* @returns {Array<{startPos: number, endPos: number}>} 指定されたフィールドの開始位置と終了位置のオブジェクトのリスト。
*/
function getStartEndRangeListWithoutDoubleQuote(text, cursorFieldCount, delimiter) {
var startEndRange = null;
var startEndRangeList = [];
var fieldCount = 1;
var inDoubleQuote = false;
var addStartEndRange = function (startEndRange) {
if (startEndRange.startPos != startEndRange.endPos) {
startEndRangeList.push(startEndRange);
}
}
for (var i = 0; i < text.length; i++) {
// 選択範囲の開始位置・終了位置を設定
if (startEndRange == null && fieldCount === cursorFieldCount) {
startEndRange = { startPos: i, endPos: i };
} else if (startEndRange != null && fieldCount === cursorFieldCount + 1) {
startEndRange.endPos = i - 1;
addStartEndRange(startEndRange);
startEndRange = null;
}
if (text.charAt(i) === delimiter) {
fieldCount++;
} else if (!inDoubleQuote && text.charAt(i) === '\n') {
fieldCount = 1;
// 改行でフィールドがリセットされるため、終了位置を設定
if (startEndRange != null) {
startEndRange.endPos = i;
addStartEndRange(startEndRange);
startEndRange = null;
}
} else if (i == text.length - 1 && startEndRange != null) {
// テキストの最後の場合
startEndRange.endPos = i + 1;
addStartEndRange(startEndRange);
}
}
return startEndRangeList;
}
スポンサーリンク