552 lines
21 KiB
C#
552 lines
21 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using UnityEngine;
|
|
using UVC.Json;
|
|
using UVC.Log;
|
|
|
|
namespace UVC.Extention
|
|
{
|
|
/// <summary>
|
|
/// 문자열 확장 메소드를 제공하는 클래스입니다.
|
|
/// </summary>
|
|
public static class StringEx
|
|
{
|
|
/// <summary>
|
|
/// 문자열을 지정된 열거형 타입으로 변환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">변환할 열거형 타입</typeparam>
|
|
/// <param name="txt">변환할 문자열</param>
|
|
/// <returns>변환된 열거형 값</returns>
|
|
/// <exception cref="ArgumentException">문자열이 열거형 값으로 변환될 수 없을 때 발생</exception>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 사용 예시
|
|
/// public enum Direction { North, South, East, West }
|
|
///
|
|
/// string dirText = "North";
|
|
/// Direction dir = dirText.ToEnum<Direction>(); // Direction.North가 반환됨
|
|
/// </code>
|
|
/// </example>
|
|
public static T ToEnum<T>(this string txt)
|
|
{
|
|
return (T)Enum.Parse(typeof(T), txt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열이 null이거나 빈 문자열인지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="value">검사할 문자열</param>
|
|
/// <returns>문자열이 null이거나 빈 문자열이면 true, 그렇지 않으면 false</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 사용 예시
|
|
/// string text1 = null;
|
|
/// string text2 = "";
|
|
/// string text3 = "Hello";
|
|
///
|
|
/// bool result1 = text1.IsNullOrEmpty(); // true 반환
|
|
/// bool result2 = text2.IsNullOrEmpty(); // true 반환
|
|
/// bool result3 = text3.IsNullOrEmpty(); // false 반환
|
|
/// </code>
|
|
/// </example>
|
|
public static bool IsNullOrEmpty(this string value)
|
|
{
|
|
return value == null || value.Length == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열이 null, 빈 문자열 또는 공백 문자로만 구성되어 있는지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="value">검사할 문자열</param>
|
|
/// <returns>문자열이 null, 빈 문자열 또는 공백 문자로만 구성되어 있으면 true, 그렇지 않으면 false</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// string text1 = null;
|
|
/// string text2 = " ";
|
|
/// string text3 = "Hello";
|
|
///
|
|
/// bool result1 = text1.IsNullOrWhiteSpace(); // true 반환
|
|
/// bool result2 = text2.IsNullOrWhiteSpace(); // true 반환
|
|
/// bool result3 = text3.IsNullOrWhiteSpace(); // false 반환
|
|
/// </code>
|
|
/// </example>
|
|
public static bool IsNullOrWhiteSpace(this string value)
|
|
{
|
|
if (value == null) return true;
|
|
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
if (!char.IsWhiteSpace(value[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열을 지정된 타입으로 변환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">변환할 타입</typeparam>
|
|
/// <param name="value">변환할 문자열</param>
|
|
/// <param name="defaultValue">변환 실패시 반환할 기본값</param>
|
|
/// <returns>변환된 값 또는 기본값</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// string numStr = "123";
|
|
/// string invalidStr = "abc";
|
|
///
|
|
/// int num1 = numStr.To<int>(); // 123 반환
|
|
/// int num2 = invalidStr.To<int>(0); // 0 반환 (기본값)
|
|
/// </code>
|
|
/// </example>
|
|
public static T To<T>(this string value, T defaultValue = default)
|
|
{
|
|
try
|
|
{
|
|
return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
|
}
|
|
catch
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열의 첫 글자만 대문자로 변환합니다.
|
|
/// </summary>
|
|
/// <param name="value">변환할 문자열</param>
|
|
/// <returns>첫 글자가 대문자로 변환된 문자열</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// string text = "hello world";
|
|
/// string result = text.Capitalize(); // "Hello world" 반환
|
|
/// </code>
|
|
/// </example>
|
|
public static string Capitalize(this string value)
|
|
{
|
|
if (value.IsNullOrEmpty()) return value;
|
|
return char.ToUpper(value[0]) + value.Substring(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 주어진 패턴을 이용하여 문자열이 정규식 패턴과 일치하는지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="value">검사할 문자열</param>
|
|
/// <param name="pattern">정규식 패턴</param>
|
|
/// <returns>문자열이 패턴과 일치하면 true, 그렇지 않으면 false</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// string email = "user@example.com";
|
|
/// string pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
|
|
///
|
|
/// bool isValid = email.IsMatch(pattern); // true 반환
|
|
/// </code>
|
|
/// </example>
|
|
public static bool IsMatch(this string value, string pattern)
|
|
{
|
|
if (value == null) return false;
|
|
return Regex.IsMatch(value, pattern);
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON 문자열을 Dictionary로 변환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="TKey">Dictionary의 키 타입</typeparam>
|
|
/// <typeparam name="TValue">Dictionary의 값 타입</typeparam>
|
|
/// <param name="json">변환할 JSON 문자열</param>
|
|
/// <returns>변환된 Dictionary 객체</returns>
|
|
/// <exception cref="ArgumentException">JSON 문자열 형식이 올바르지 않은 경우 발생</exception>
|
|
/// <example>
|
|
/// <code>
|
|
/// string json = "{\"name\":\"홍길동\",\"age\":30,\"isStudent\":false}";
|
|
/// var dict = json.ToDictionaryFromJson<string, object>();
|
|
///
|
|
/// // 결과:
|
|
/// // dict["name"] == "홍길동"
|
|
/// // dict["age"] == 30
|
|
/// // dict["isStudent"] == false
|
|
///
|
|
/// // 중첩된 객체도 처리 가능
|
|
/// string nestedJson = "{\"user\":{\"name\":\"홍길동\",\"address\":{\"city\":\"서울\"}}}";
|
|
/// var nestedDict = nestedJson.ToDictionaryFromJson<string, object>();
|
|
/// </code>
|
|
/// </example>
|
|
public static Dictionary<TKey, TValue> ToDictionaryFromJson<TKey, TValue>(this string json)
|
|
{
|
|
if (json.IsNullOrWhiteSpace())
|
|
{
|
|
return new Dictionary<TKey, TValue>();
|
|
}
|
|
|
|
// JSON 객체가 아닌 경우 예외 발생
|
|
json = json.Trim();
|
|
if (!json.StartsWith("{") || !json.EndsWith("}"))
|
|
throw new ArgumentException("유효한 JSON 객체 형식이 아닙니다.", nameof(json));
|
|
|
|
try
|
|
{
|
|
// JsonHelper를 사용하여 Dictionary로 변환
|
|
return JsonHelper.FromJson<Dictionary<TKey, TValue>>(json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 변환 실패 시 예외 정보 로깅 (선택적)
|
|
ULog.Error($"JSON을 Dictionary로 변환하는 중 오류 발생: {ex.Message}", ex);
|
|
return new Dictionary<TKey, TValue>();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// JSON 문자열을 Dictionary로 변환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="TKey">Dictionary의 키 타입 (일반적으로 string)</typeparam>
|
|
/// <typeparam name="TValue">Dictionary의 값 타입</typeparam>
|
|
/// <param name="json">변환할 JSON 문자열</param>
|
|
/// <returns>JSON 문자열에서 파싱된 Dictionary</returns>
|
|
/// <exception cref="ArgumentException">JSON 문자열 형식이 올바르지 않은 경우 발생</exception>
|
|
/// <example>
|
|
/// <code>
|
|
/// string jsonStr = "{\"name\":\"홍길동\",\"age\":30,\"isStudent\":false}";
|
|
/// var dict = jsonStr.ToDictionary<string, object>();
|
|
///
|
|
/// // 결과:
|
|
/// // dict["name"] = "홍길동"
|
|
/// // dict["age"] = 30
|
|
/// // dict["isStudent"] = false
|
|
/// </code>
|
|
/// </example>
|
|
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this string json)
|
|
{
|
|
// JSON 문자열이 null이거나 비어있는 경우
|
|
if (json.IsNullOrWhiteSpace())
|
|
return new Dictionary<TKey, TValue>();
|
|
|
|
// JSON 객체가 아닌 경우 예외 발생
|
|
json = json.Trim();
|
|
if (!json.StartsWith("{") || !json.EndsWith("}"))
|
|
throw new ArgumentException("유효한 JSON 객체 형식이 아닙니다.", nameof(json));
|
|
|
|
// Dictionary 생성
|
|
var result = new Dictionary<TKey, TValue>();
|
|
|
|
// 중괄호 제거 후 내용만 추출
|
|
json = json.Substring(1, json.Length - 2).Trim();
|
|
if (string.IsNullOrEmpty(json))
|
|
return result;
|
|
|
|
int startIndex = 0;
|
|
while (startIndex < json.Length)
|
|
{
|
|
// 키 추출
|
|
int keyStartIndex = json.IndexOf('"', startIndex);
|
|
if (keyStartIndex == -1)
|
|
break;
|
|
|
|
int keyEndIndex = FindClosingQuote(json, keyStartIndex + 1);
|
|
if (keyEndIndex == -1)
|
|
throw new ArgumentException("JSON 키 형식이 올바르지 않습니다.", nameof(json));
|
|
|
|
string keyStr = json.Substring(keyStartIndex + 1, keyEndIndex - keyStartIndex - 1);
|
|
|
|
// : 찾기
|
|
int colonIndex = json.IndexOf(':', keyEndIndex + 1);
|
|
if (colonIndex == -1)
|
|
throw new ArgumentException("JSON 형식이 올바르지 않습니다: 콜론이 없습니다.", nameof(json));
|
|
|
|
// 값의 시작 위치
|
|
int valueStartIndex = FindNextNonWhitespaceIndex(json, colonIndex + 1);
|
|
if (valueStartIndex >= json.Length)
|
|
break;
|
|
|
|
// 값 유형에 따른 처리
|
|
object value;
|
|
int nextStartIndex;
|
|
|
|
char firstChar = json[valueStartIndex];
|
|
switch (firstChar)
|
|
{
|
|
case '{': // 중첩된 객체
|
|
int objectEndIndex = FindMatchingClosingBrace(json, valueStartIndex);
|
|
string objectJson = json.Substring(valueStartIndex, objectEndIndex - valueStartIndex + 1);
|
|
|
|
// 재귀적으로 중첩 객체 처리 (최종 타입에 따라 처리)
|
|
if (typeof(TValue) == typeof(Dictionary<string, object>))
|
|
{
|
|
value = objectJson.ToDictionary<string, object>();
|
|
}
|
|
else if (typeof(TValue) == typeof(Dictionary<string, string>))
|
|
{
|
|
value = objectJson.ToDictionary<string, string>();
|
|
}
|
|
else
|
|
{
|
|
// 기본적으로 문자열로 저장
|
|
value = objectJson;
|
|
}
|
|
|
|
nextStartIndex = objectEndIndex + 1;
|
|
break;
|
|
|
|
case '[': // 배열(현재 구현에서는 단순 문자열로 처리)
|
|
int arrayEndIndex = FindMatchingClosingBracket(json, valueStartIndex);
|
|
value = json.Substring(valueStartIndex, arrayEndIndex - valueStartIndex + 1);
|
|
nextStartIndex = arrayEndIndex + 1;
|
|
break;
|
|
|
|
case '"': // 문자열
|
|
int stringEndIndex = FindClosingQuote(json, valueStartIndex + 1);
|
|
string stringValue = json.Substring(valueStartIndex + 1, stringEndIndex - valueStartIndex - 1);
|
|
value = UnescapeJsonString(stringValue);
|
|
nextStartIndex = stringEndIndex + 1;
|
|
break;
|
|
|
|
default: // 숫자, boolean, null
|
|
int commaOrEndIndex = json.IndexOfAny(new[] { ',', '}' }, valueStartIndex);
|
|
if (commaOrEndIndex == -1)
|
|
commaOrEndIndex = json.Length;
|
|
|
|
string primitiveValueStr = json.Substring(valueStartIndex, commaOrEndIndex - valueStartIndex).Trim();
|
|
|
|
// null, boolean, 숫자 처리
|
|
if (primitiveValueStr == "null")
|
|
{
|
|
value = null;
|
|
}
|
|
else if (primitiveValueStr == "true")
|
|
{
|
|
value = true;
|
|
}
|
|
else if (primitiveValueStr == "false")
|
|
{
|
|
value = false;
|
|
}
|
|
else if (double.TryParse(primitiveValueStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double numValue))
|
|
{
|
|
// 정수인지 실수인지 확인
|
|
if (Math.Floor(numValue) == numValue && numValue >= int.MinValue && numValue <= int.MaxValue)
|
|
{
|
|
value = (int)numValue;
|
|
}
|
|
else
|
|
{
|
|
value = numValue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
value = primitiveValueStr; // 숫자로 파싱할 수 없다면 문자열로 처리
|
|
}
|
|
|
|
nextStartIndex = commaOrEndIndex;
|
|
break;
|
|
}
|
|
|
|
// 키와 값을 딕셔너리에 추가 (형변환 시도)
|
|
try
|
|
{
|
|
var key = (TKey)Convert.ChangeType(keyStr, typeof(TKey), CultureInfo.InvariantCulture);
|
|
result[key] = value is TValue tValue ? tValue : (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture);
|
|
}
|
|
catch
|
|
{
|
|
// 형변환 실패 시 건너뜀
|
|
}
|
|
|
|
// 다음 키-값 쌍의 시작 위치 찾기
|
|
startIndex = json.IndexOf(',', nextStartIndex);
|
|
if (startIndex == -1)
|
|
break;
|
|
|
|
startIndex++; // 콤마 다음 위치로 이동
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON 문자열의 이스케이프된 문자를 원래 문자로 변환합니다.
|
|
/// </summary>
|
|
private static string UnescapeJsonString(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return value;
|
|
|
|
StringBuilder sb = new StringBuilder(value.Length);
|
|
int i = 0;
|
|
|
|
while (i < value.Length)
|
|
{
|
|
char c = value[i++];
|
|
if (c == '\\' && i < value.Length)
|
|
{
|
|
char escapeChar = value[i++];
|
|
switch (escapeChar)
|
|
{
|
|
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;
|
|
case 'u':
|
|
if (i + 4 <= value.Length)
|
|
{
|
|
string unicodeHex = value.Substring(i, 4);
|
|
if (int.TryParse(unicodeHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int unicodeValue))
|
|
{
|
|
sb.Append((char)unicodeValue);
|
|
i += 4;
|
|
}
|
|
else
|
|
{
|
|
sb.Append(escapeChar);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.Append(escapeChar);
|
|
}
|
|
break;
|
|
default: sb.Append(escapeChar); break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON 문자열에서 닫는 따옴표의 위치를 찾습니다.
|
|
/// </summary>
|
|
private static int FindClosingQuote(string json, int startIndex)
|
|
{
|
|
for (int i = startIndex; i < json.Length; i++)
|
|
{
|
|
if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\'))
|
|
return i;
|
|
|
|
if (json[i] == '\\')
|
|
i++; // 이스케이프된 문자 건너뛰기
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 주어진 위치 이후의 첫 번째 공백이 아닌 문자의 위치를 반환합니다.
|
|
/// </summary>
|
|
private static int FindNextNonWhitespaceIndex(string text, int startIndex)
|
|
{
|
|
for (int i = startIndex; i < text.Length; i++)
|
|
{
|
|
if (!char.IsWhiteSpace(text[i]))
|
|
return i;
|
|
}
|
|
return text.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 여는 중괄호에 대응하는 닫는 중괄호의 위치를 찾습니다.
|
|
/// </summary>
|
|
private static int FindMatchingClosingBrace(string json, int openBraceIndex)
|
|
{
|
|
int depth = 1;
|
|
bool inString = false;
|
|
|
|
for (int i = openBraceIndex + 1; i < json.Length; i++)
|
|
{
|
|
if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\'))
|
|
{
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
|
|
if (inString)
|
|
continue;
|
|
|
|
if (json[i] == '{')
|
|
{
|
|
depth++;
|
|
}
|
|
else if (json[i] == '}')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 여는 대괄호에 대응하는 닫는 대괄호의 위치를 찾습니다.
|
|
/// </summary>
|
|
private static int FindMatchingClosingBracket(string json, int openBracketIndex)
|
|
{
|
|
int depth = 1;
|
|
bool inString = false;
|
|
|
|
for (int i = openBracketIndex + 1; i < json.Length; i++)
|
|
{
|
|
if (json[i] == '\"' && (i == 0 || json[i - 1] != '\\'))
|
|
{
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
|
|
if (inString)
|
|
continue;
|
|
|
|
if (json[i] == '[')
|
|
{
|
|
depth++;
|
|
}
|
|
else if (json[i] == ']')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 리소스 경로에서 지정된 유형의 Unity 리소스를 로드합니다.
|
|
/// </summary>
|
|
/// <remarks>이 메서드는 Unity의 <see cref="UnityEngine.Resources.Load{T}(string)"/> 메서드를 사용하여 지정된 경로에서 리소스를 검색합니다. 리소스를 찾을 수 없거나 경로가 유효하지 않으면
|
|
/// 오류가 기록되고 <see langword="null"/>이 반환됩니다.</remarks>
|
|
/// <typeparam name="T">로드할 리소스의 유형입니다. <see cref="UnityEngine.Object"/>에서 파생되어야 합니다.</typeparam>
|
|
/// <param name="resourcePath">Unity 리소스 폴더 내 리소스 경로입니다. null이거나 비어 있을 수 없습니다.</param>
|
|
/// <returns> <typeparamref name="T"/> 유형의 로드된 리소스가 발견되면 해당 리소스를 반환합니다. 그렇지 않으면 <see langword="null"/>.</returns>
|
|
public static T? LoadResource<T>(this string resourcePath) where T : UnityEngine.Object
|
|
{
|
|
if (string.IsNullOrEmpty(resourcePath))
|
|
{
|
|
Debug.LogError("리소스 경로가 비어있습니다.");
|
|
return null;
|
|
}
|
|
T resource = Resources.Load<T>(resourcePath);
|
|
if (resource == null)
|
|
{
|
|
Debug.LogError($"리소스를 찾을 수 없습니다: {resourcePath}");
|
|
}
|
|
return resource;
|
|
|
|
}
|
|
}
|
|
}
|