• Showing Page History #121847

Show page source of resolve_duplicate_name #121850

= オブジェクトリストでオブジェクトの名前が重複しないように連番を振る処理
== はじめに
ドローソフトや、CAD、3Dレンダリングソフト等では、キャンバスや3D空間に複数のオブジェクトを配置していくようなソフトの作りになっていることが多いと思います。
それらのオブジェクトはリストで管理できるようになっていて、ソフトによってはリスト上でリネームができたり、外部ファイルを介したオブジェクト読み書きができるかもしれません。
 * 例:Inkscapeのオブジェクトリスト
[[Embed(example_of_object_list.png)]]

ここで仮に、「オブジェクト名の重複禁止」の要件を含むソフトを作るケースを考えます。
その制約条件下では、リネームにより他のオブジェクトと名前が重複してしまう可能性がありますので、
リネーム処理の際に例えば
 * 名前が重複した場合はにエラーメッセージを出してリネームを強制キャンセルする
 * リネームした名前の末尾に連番を振る
といった処理を加えることになります。
外部ファイル等から複数のオブジェクトを一括でインポートしたいときにも同様の処置が必要になるはずです。

そこで、こういう状況で役に立つことを期待して「リネーム、一括インポート時にオブジェクト名が重複しないように連番を振る処理」を作ってみましたので紹介します。

== 処理例
本プログラムは、オブジェクト名重複チェックと自動リネームを一度に実施する関数 ResolveDuplicateName と、その動作を見るためのGUIサンプルで構成されています。
リネーム処理、一括インポート処理のそれぞれを模した例を示します。
 * リネーム処理
[[Embed(auto_numbering_on_rename.png)]]

 * インポート処理
[[Embed(auto_numbering_on_import.png)]]

== ライセンス
 * Boost License v1.0

== ソース
 * [/downloads/users/40/40027/resolve_duplicate_name.zip/ ダウンロード]

ResolveDuplicateName の内容
{{{
/// ==============
///    処理本体
/// ==============
/// <summary>
/// 名前リストについて、名前の重複を調べ、重複している名前については連番をふり直す。
/// </summary>
/// <param name="nameList">名前リスト</param>
/// <param name="renamableBegin">連番ふり直し対象の開始インデックス</param>
/// <param name="renamableEnd">連番ふり直し対象の終了インデックス</param>
/// <returns></returns>
static public int[] ResolveDuplicateName(string[] nameList, int renamableBegin, int renamableEnd) {
	int begin = 0, end = nameList.Length;

	// [処理1] 名前の重複数をカウントする
	// Key:末尾の連番が残ったままの名前、 Value:重複カウント数
	var orgNameDuplicateCount = new Dictionary<string, int>();
	for (int i = begin; i < end; ++i) {
		var name = nameList[i];
		if (string.IsNullOrEmpty(name)) continue;
		int cnt = 0;
		if (!orgNameDuplicateCount.TryGetValue(name, out cnt)) {
			orgNameDuplicateCount.Add(name, 1);
		} else {
			orgNameDuplicateCount[name] = cnt + 1;
		}
	}

	// [処理2] 名前から末尾の連番部分を外し、末尾の連番を外した名前毎に連番の最大値を取得する
	// [処理3] 末尾の連番部分を外した名前のリストを作成する
	// Key:末尾の連番を外した名前、 Value:ついている番号の最大値 
	var wnNameMaxNumber = new Dictionary<string, int>();
	var wnNames = new string[end - begin];
	for (int i = begin; i < end; ++i) {
		var nameWithoutNumber = "";
		var name = nameList[i];
		if (string.IsNullOrEmpty(name)) continue;
		int suffixNumber = -1;
		var lastPeriodIndex = name.LastIndexOf('.');
		if (lastPeriodIndex > 0) {
			nameWithoutNumber = name.Substring(0, lastPeriodIndex);
			var suffix = name.Substring(lastPeriodIndex + 1);
			int num;
			if (int.TryParse(suffix, out num) && num >= 0) {
				suffixNumber = num;
			}
		} else {
			nameWithoutNumber = name;
			suffixNumber = -1;
		}
		wnNames[i - begin] = nameWithoutNumber;
		int lastMaxNumber = -1;
		if (!wnNameMaxNumber.TryGetValue(nameWithoutNumber, out lastMaxNumber)) {
			wnNameMaxNumber.Add(nameWithoutNumber, suffixNumber);
		} else {
			if (lastMaxNumber == -1 && suffixNumber == -1) suffixNumber = 0;
			if (lastMaxNumber < suffixNumber) wnNameMaxNumber[nameWithoutNumber] = suffixNumber;
		}
	}

	// [処理4] 重複している名前に連番をふり直して重複解除する。
	// [処理5] 連番をふり直した要素インデックスを記録する。
	var resolved = new List<int>();
	if (renamableBegin < begin) renamableBegin = begin;
	if (renamableEnd > end) renamableEnd = end;
	for (int i = renamableBegin; i < renamableEnd; ++i) {
		var name = nameList[i];
		if (string.IsNullOrEmpty(name)) continue;
		if (name[name.Length - 1] == '.') name = name.Substring(0, name.Length - 1);
		bool nameNumberless = (name == wnNames[i - begin]);

		// 末尾に連番がついていない名前の場合は、連番がついているものと重複判定する。
		// 末尾に連番がついている名前の場合は、重複してなければ名前変更不要。
		if (!nameNumberless && orgNameDuplicateCount[name] == 1) continue;

		var nameWithoutNumber = wnNames[i - begin];
		int maxNumber;
		if (!wnNameMaxNumber.TryGetValue(nameWithoutNumber, out maxNumber)) continue;
		if (nameNumberless && maxNumber == -1) continue;
		nameList[i] = string.Format("{0}.{1}", nameWithoutNumber, maxNumber + 1);
		resolved.Add(i);
		wnNameMaxNumber[nameWithoutNumber] = maxNumber + 1;
	}
	return resolved.ToArray();
}
}}}