CSV/TSVでアクティブ列選択

提供: MeryWiki
2024年12月28日 (土) 20:48時点におけるYuko (トーク | 投稿記録)による版
ナビゲーションに移動 検索に移動

概要

このマクロは、CSVファイル内でアクティブな列を選択するためのものです。現在のカーソル位置を基準に、対応する列を自動的に全て選択します。

ダブルクォーテーション

注意事項

  • Mery Ver 3.7.9 で動作確認しています。

使い方

  1. MeryでCSVファイルを開く
  2. 選択したい列にカーソルを移動させる
  3. このマクロを実行すると、カーソル位置に対応する列が全て選択される

ソースコード

※2024/12/27 現在、ダブルクォーテーション内のカンマでフィールドがずれる問題や動作が重くなる問題を確認しています。改善に向け修正中です。

#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);
    }
};

var doc = document;
var sel = doc.selection;

main();

function main() {
    // 現在の列位置を取得
    var actPos = sel.GetActivePos();
    while (true) {
        // CSVかTSVかコンテキストメニューで指定
        var menu = createContextMenu();
        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) {
    for (var i = actPos; i >= 0; i--) {
        var checkString = text.charAt(i - 1) + text.charAt(i)
        // ダブルクォーテーション範囲閉じ位置に到達したので終了
        if (actPos != i && (checkString === '"' + delimiter || checkString === '"\n')) {
            return -1;
        }
        // ダブルクォーテーション範囲始まり位置を発見したらその位置を返す
        if (checkString === delimiter + '"' || checkString === '"') {
            return i;
        }
    }
    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--) {
        if (text.charAt(i) === delimiter) {
            var checkString1 = text.charAt(i) + text.charAt(i + 1)
            var checkString2 = text.charAt(i - 1) + text.charAt(i)
            // ダブルクォーテーション範囲から抜ける
            if (inDoubleQuote && checkString1 === delimiter + '"') {
                inDoubleQuote = false;
            }
            // ダブルクォーテーション範囲内のデリミタはカウントしない
            if (!inDoubleQuote) {
                fieldCount++;
            }
            // ダブルクォーテーション範囲に入る
            if (checkString2 === '"' + delimiter) {
                inDoubleQuote = true;
            }
        } else if (inDoubleQuote && text.charAt(i) === '"' && (text.charAt(i - 1) === '\n' || i === 0)) {
            break;
        } else if (text.charAt(i) === '\n' && !inDoubleQuote) {
            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);
        }
    }

    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;
        }

        var checkString1 = text.charAt(i - 1) + text.charAt(i);
        var checkString2 = text.charAt(i) + text.charAt(i + 1);
        if (text.charAt(i) === delimiter) {
            // ダブルクォーテーション範囲から抜ける
            if (inDoubleQuote && checkString1 === '"' + delimiter) {
                inDoubleQuote = false;
            }
            // ダブルクォーテーション範囲内のデリミタはカウントしない
            if (!inDoubleQuote) {
                fieldCount++;
            }
            // ダブルクォーテーション範囲に入る
            if (checkString2 === delimiter + '"') {
                inDoubleQuote = true;
            }
        } else if (inDoubleQuote && text.charAt(i) === '"' && (text.charAt(i + 1) === '\n' || text.charAt(i + 1) === '')) {
            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;
}
スポンサーリンク