Files
XRLib/Assets/Scripts/UVC/Extention/DictionaryEx.cs
2025-06-19 19:24:30 +09:00

315 lines
13 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace UVC.Extention
{
/// <summary>
/// Dictionary 클래스에 대한 확장 메소드를 제공하는 클래스입니다.
/// </summary>
public static class DictionaryEx
{
/// <summary>
/// Dictionary를 JSON 문자열로 변환합니다.
/// 중첩된 Dictionary 객체도 재귀적으로 처리합니다.
/// </summary>
/// <typeparam name="TKey">Dictionary의 키 타입</typeparam>
/// <typeparam name="TValue">Dictionary의 값 타입</typeparam>
/// <param name="dict">JSON 문자열로 변환할 Dictionary 객체</param>
/// <returns>JSON 형식의 문자열</returns>
/// <example>
/// <code>
/// var dict = new Dictionary<string, object>
/// {
/// { "name", "홍길동" },
/// { "age", 30 },
/// { "address", new Dictionary<string, string> { { "city", "서울" }, { "zip", "12345" } } }
/// };
///
/// string json = dict.ToJson();
/// // 결과: {"name":"홍길동","age":"30","address":{"city":"서울","zip":"12345"}}
/// </code>
/// </example>
public static string ToJson<TKey, TValue>(this IDictionary<TKey, TValue> dict)
{
if (dict == null || dict.Count == 0)
return "{}";
var sb = new StringBuilder("{");
foreach (var kvp in dict)
{
sb.Append($"\"{kvp.Key}\":");
// 값이 null인 경우 처리
if (kvp.Value == null)
{
sb.Append("null,");
continue;
}
// 값이 중첩된 Dictionary인 경우 재귀적으로 처리
if (kvp.Value is IDictionary<object, object> dictObject)
{
string nestedJson = ToJson(dictObject);
sb.Append($"{nestedJson},");
}
else if (kvp.Value is IDictionary<string, object> dictStringObject)
{
string nestedJson = ToJson(dictStringObject);
sb.Append($"{nestedJson},");
}
else if (kvp.Value is IDictionary<string, string> dictStringString)
{
string nestedJson = ToJson(dictStringString);
sb.Append($"{nestedJson},");
}
// 값이 숫자인 경우 따옴표 없이 추가
else if (IsNumericType(kvp.Value.GetType()))
{
sb.Append($"{kvp.Value},");
}
// 값이 불리언인 경우 따옴표 없이 추가 (소문자 true/false)
else if (kvp.Value is bool boolValue)
{
sb.Append($"{boolValue.ToString().ToLowerInvariant()},");
}
// 그 외의 경우 문자열로 처리
else
{
// 특수 문자를 이스케이프 처리
string escapedValue = EscapeJsonString(kvp.Value.ToString());
sb.Append($"\"{escapedValue}\",");
}
}
if (dict.Count > 0)
{
sb.Length--; // 마지막 쉼표(,) 제거
}
sb.Append("}");
return sb.ToString();
}
/// <summary>
/// 주어진 타입이 숫자형인지 확인합니다.
/// </summary>
/// <param name="type">검사할 타입</param>
/// <returns>숫자형이면 true, 아니면 false</returns>
private static bool IsNumericType(Type type)
{
if (type == null)
return false;
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return true;
default:
return false;
}
}
/// <summary>
/// JSON 문자열에서 특수 문자를 이스케이프 처리합니다.
/// </summary>
/// <param name="value">이스케이프 처리할 문자열</param>
/// <returns>이스케이프 처리된 문자열</returns>
private static string EscapeJsonString(string value)
{
if (string.IsNullOrEmpty(value))
return value;
StringBuilder sb = new StringBuilder();
foreach (char c in value)
{
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '\"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
default:
if (c < 32) // 제어 문자
{
sb.Append($"\\u{(int)c:X4}");
}
else
{
sb.Append(c);
}
break;
}
}
return sb.ToString();
}
/// <summary>
/// 두 사전의 키와 값을 비교하여 두 사전이 같은지 확인합니다.
/// </summary>
/// <typeparam name="TKey">사전의 키 유형입니다.</typeparam>
/// <typeparam name="TValue">사전의 값 유형입니다.</typeparam>
/// <param name="dic">비교할 첫 번째 사전입니다.</param>
/// <param name="other">비교할 두 번째 사전입니다.</param>
/// <paramref name="dic"/>의 모든 키가 <paramref name="other"/>에 존재하고 해당 값이 같으면 <see langword="true"/>를 반환합니다.
/// 그렇지 않으면 <see langword="false"/>를 반환합니다.</returns>
public static bool IsSame<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> other)
{
return !dic.Keys.Any(key => !object.Equals(dic[key], other[key]));
}
/// <summary>
/// 두 사전을 비교하여 두 번째 사전의 키와 값을 포함하는 새 사전을 반환합니다.
/// 두 사전의 값이 첫 번째 사전의 값과 다릅니다.
/// </summary>
/// <remarks>이 메서드는 <see
/// cref="object.Equals(object, object)"/>를 사용하여 얕은 값 비교를 수행합니다. 키가 <paramref name="oldDict"/>에는 있지만 <paramref
/// name="newDict"/>에는 없는 경우, 또는 그 반대의 경우, 해당 키는 무시됩니다.</remarks>
/// <typeparam name="TKey">사전에 있는 키의 타입입니다.</typeparam>
/// <typeparam name="TValue">사전에 있는 값의 타입입니다.</typeparam>
/// <param name="oldDict">비교할 원본 사전입니다.</param>
/// <param name="newDict">비교할 업데이트된 사전입니다.</param>
/// <returns> <paramref name="newDict"/>의 키가 포함된 사전입니다. 해당 키의 값이 <paramref name="oldDict"/>의 해당 값과 다른 경우입니다.
/// 반환된 사전의 값은 <paramref name="newDict"/>에서 가져옵니다.</returns>
public static IDictionary<TKey, TValue> Differences<TKey, TValue>(this IDictionary<TKey, TValue> oldDict, IDictionary<TKey, TValue> newDict)
{
var result = new Dictionary<TKey, TValue>();
foreach (var key in oldDict.Keys)
{
TValue oldValue = oldDict[key];
TValue newValue = newDict[key];
if (!object.Equals(oldValue, newValue))
{
result.Add(key, newValue);
}
}
return result;
}
/// <summary>
/// 두 사전을 비교하여 두 번째 사전의 키와 값을 포함하는 새 사전을 반환합니다.
/// 두 사전의 값이 첫 번째 사전의 값과 다릅니다.
/// </summary>
/// <remarks>이 메서드는 심층 비교를 사용하여 값이 다른지 확인합니다. 비교
/// 로직은 <c>DeepEquals</c> 메서드로 정의됩니다. <paramref name="dict1"/>에는 있지만
/// <paramref name="dict2"/>에는 없는 키는 결과에 포함되지 않습니다.</remarks>
/// <typeparam name="TKey">사전에 있는 키의 타입입니다.</typeparam>
/// <typeparam name="TValue">사전에 있는 값의 타입입니다.</typeparam>
/// <param name="dict1">비교할 첫 번째 사전입니다.</param>
/// <param name="dict2">비교할 두 번째 사전입니다.</param>
/// <returns><paramref name="dict2"/>의 키와 값을 포함하는 사전으로, 값이 <paramref name="dict1"/>의 값과 다른 경우
/// <paramref name="dict1"/>에 키가 있지만 값이 같지 않으면
/// <paramref name="dict2"/>의 키와 해당 값이 포함됩니다.</returns>
public static IDictionary<TKey, TValue> DeepDifferences<TKey, TValue>(this IDictionary<TKey, TValue> dict1, IDictionary<TKey, TValue> dict2)
{
var result = new Dictionary<TKey, TValue>();
foreach (var key in dict1.Keys)
{
TValue val1 = dict1[key];
TValue val2 = dict2[key];
if (!DeepEquals(val1, val2))
{
result.Add(key, val2);
}
}
return result;
}
/// <summary>
/// 두 객체의 값, 속성 및 중첩 구조를 비교하여 두 객체가 깊이 동일한지 여부를 확인합니다.
///
// </summary>
/// <remarks>이 메서드는 제공된 객체와 중첩 구조를 재귀적으로 비교합니다.
/// 기본 유형, 문자열, 소수, 컬렉션(예: 배열,리스트), 사전 및 복합 객체의 비교를 지원합니다.
/// 컬렉션의 경우, 이 메서드는 요소를 순서대로 비교합니다.
/// 사전의 경우, 키와 연관된 값을 비교합니다. 객체의 유형이 다르거나 일치하지 않는 구조를 가진 경우,
/// 이 메서드는 <see langword="false"/>를 반환합니다.</remarks>
/// <param name="obj1">비교할 첫 번째 객체입니다. null일 수 있습니다.</param>
/// <param name="obj2">비교할 두 번째 객체입니다. null일 수 있습니다.</param>
/// 객체가 깊이 동일하면 <returns><see langword="true"/>를 반환하고, 그렇지 않으면 <see langword="false"/>를 반환합니다. 깊이 동일성
///에는 기본 값, 문자열, 컬렉션, 사전 및 복합 객체의 공개 속성 비교가 포함됩니다.
///</returns>
private static bool DeepEquals(object obj1, object obj2)
{
if (ReferenceEquals(obj1, obj2))
return true;
if (obj1 == null || obj2 == null)
return false;
if (obj1.Equals(obj2))
return true;
var type1 = obj1.GetType();
var type2 = obj2.GetType();
if (type1 != type2)
return false;
// String or primitive
if (type1.IsPrimitive || obj1 is string || obj1 is decimal)
return obj1.Equals(obj2);
// List or Array
if (obj1 is IEnumerable enumerable1 && obj2 is IEnumerable enumerable2)
{
var list1 = enumerable1.Cast<object>().ToList();
var list2 = enumerable2.Cast<object>().ToList();
if (list1.Count != list2.Count)
return false;
for (int i = 0; i < list1.Count; i++)
{
if (!DeepEquals(list1[i], list2[i]))
return false;
}
return true;
}
// Dictionary
if (obj1 is IDictionary dict1 && obj2 is IDictionary dict2)
{
if (dict1.Count != dict2.Count)
return false;
foreach (var key in dict1.Keys)
{
if (!dict2.Contains(key)) return false;
if (!DeepEquals(dict1[key], dict2[key])) return false;
}
return true;
}
// Complex object
var props = type1.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
var val1 = prop.GetValue(obj1);
var val2 = prop.GetValue(obj2);
if (!DeepEquals(val1, val2))
return false;
}
return true;
}
}
}