CsvUtility 소개

PenguinGod·2022년 9월 11일
0

주의사항
이 에셋은 유니티에서 퇴짜맞아서 현재 수정을 하고 있는 중입니다. 즉, 아직 에셋 스토어에 공개되지 않았습니다. 원래는 블로그도 비공개로 하고 있었는데 다른 포스팅에서 CsvUtility를 사용했기 때문에 정보 제공 용도로 사용하기 위해 공개로 돌렸습니다.
혹시 지금 버전을 사용하고 싶으시다면 제 깃헙 링크에 가셔서 바로 보이시는 파일 중 CsvParsers.cs, CsvUtility.cs 이 두파일을 복붙 or 다운로드해서 사용하시면 됩니다. 아니면 글 마지막에 있는 전체 코드를 복붙하셔도 됩니다.

CsvUtility란?

결론부터 말하자면 그냥 제가 만든 유니티 에셋입니다.
굳이굳이 만든 이유는 게임을 개발하다가 데이터를 저장하고 Load할 일이 생겼는데 이걸 Json으로 하기에는 작성하기가 귀찮더라고요?

저걸 일일히 작성하고 수정하는 건 거 같아서 Csv로 하면 좋겠다!! 싶었습니다.
근데 Csv는 Json과 달리 별개로 Load하는 스크립트를 작성해야 했습니다.

이런 식으로 몇 번째 셀에 어떤 자료형이 있는지 확인해 그에 맞게 직접 형변환을 해줘야 합니다.
문제는 각각 사용하는 데이터가 다르다 보니, csv파일 하나가 추가 될 때마다 로드하는 코드를 추가로 작성해야 했습니다.

그게 영 마음에 안들었습니다. JsonUtility처럼 한 줄만에 뽝! 하고 되는 그런 느낌을 원했습니다. 마치 아래 사진처럼요.

처음에는 기본형만 하려고 했습니다. csv로드하는 예시를 들 때 보여드린 코드는 사실 제가 게임 개발 중에 작성한 코드입니다. 저런 형식의 코드가 반복될 거 같아서 그거만 해결하려고 했죠.

하지만 하다보니 배열도 필요해지고, 딕셔너리도 필요해지고, 내부 클래스도 필요해지고 이것저것 넣다보니 아예 에셋으로 만들자! 해서 만들게 되었습니다.

제가 처음에 예상한 것에 비해 10배도 넘게 시간을 잡아먹기는 했지만, 그래도 데이터 로드하는 데 들이는 시간을 줄인 것 같아서 꽤 만족스럽습니다.

그리고 에셋 소개글에도 적어놓았지만 여러분이 이 에셋을 사용해 귀찮은 파일 IO 시스템을 구현하는데 들이는 시간을 줄이고, 결국 여러분이 만들 위대한 게임에 조금이나마 도움이 된다면, 정말정말 기쁠 것 같습니다.

에셋 소개글에서 설명 보러 오신 분들은 재미도 없는 이야기를 읽고 계실텐데 이제 바로 설명 들어가도록 하겠습니다.

우선 CsvUtility를 사용하려면 Csv 파일을 작성하실 때 특정한 규칙에 따라서 파일을 작성하셔야 합니다. 마치 Json에 일정한 규칙이 있는 것처럼 말이죠. 하지만 그 규칙이 어렵거나 복잡하지는 않으니 너무 걱정하지 않으셔도 됩니다.

그리고 CsvUtility는 로드와 세이브를 할 때 기본적으로 데이터를 특정 타입의 배열이나 리스트를 넘기거나 받아옵니다.

배열로 예시를 들자면, csv text를 넘기면 T[]를 뱉고, T[]을 넘기면 csv text를 받아옵니다.

Load

1. 기본형

우선 데이터를 담을 간단한 컨테이너 클래스를 정의하셔야 합니다.

간단한 클래스를 정의했습니다. 그리고 CsvUtility에 적용되기 위해서는 무조건!! public이거나 [SerializeField] 속성을 가지고 있어야 합니다. 참고로 이 규칙은 JsonUtility와 동일합니다.

기본형 규칙은 단 하나입니다.
첫 번째 줄은 변수 이름 그 아래로는 쭉 데이터.

보시면 빨간색 박스 안에 첫 번째 컬럼은 변수 이름 아래로는 쭉 값입니다.

이제 저 파일을 어떻게 로드하는지 봅시다.

Resources.Load, 에셋 번들 등 파일을 가져오는 방법은 많지만, 지금은 간단한 예제를 보여주는 것이므로 인스펙터에 드래그 드롭해서 가져오도록 하겠습니다.

보시면 Test() 함수의 첫 번째 줄을 입력하면 Load 작업은 끝납니다.

blogTests = CsvUtility.CsvToArray<BlogTest>(csvAsset.text);

그 다음에 반복문은 KeyValuePair 자료형은 인스팩터에서 보이지 않으므로 결과를 확인하기 위한 코드입니다.

그럼 결과를 확인해봅시다.


코드를 실행하기 전 인스팩터 모습입니다.

[ContextMeun] 를 통해 코드를 실행하겠습니다.

깔끔하게 가져와졌습니다.

로그를 통해 pair도 잘 가져와진 것을 볼 수 있습니다.

2. 배열, 리스트, 딕셔너리

기본형과 비슷한 규칙을 사용합니다. 2개 이상의 값을 가질 경우에는 ,로 값을 구분합니다.

로드할 변수들입니다.

코드입니다. 똑같은 클래스를 바꿔서 사용하기 때문에 로드하는 부분의 변화는 없습니다.
하지만 Dictionary는 인스팩터에서 확인하지 못하기 때문에 그걸 확인하기 위해 2중 반복문을 넣었습니다.

로드할 데이터입니다. 이번에는 바로 결과를 보여드리겠습니다.

2-1. 2개 이상의 셀

배열, 리스트, 딕셔너리는 하나의 셀에 모든 데이터를 넣기에 불편한 경우가 있습니다. 특히 딕셔너리가 그러하죠.

이를 위해 2개 이상의 셀에 데이터를 연속적으로 넣을 수 있게 하였습니다. 말로 하면 애매하니까 바로 사진을 보여드리겠습니다.

보시면 첫 번째 변수명을 입력하는 컬럼에 2번 이상 연속으로 이름을 입력했습니다.
이 경우 같은 이름을 가진 모든 셀을 하나로 뭉쳐서 가져옵니다.
intArr의 2번째 줄의 경우를 보면 1,2 그리고 3,4 를 2개의 셀에 입력했는데 이 경우 로드하는 배열 값은 [1, 2, 3, 4] 입니다.

바로 결과를 봐보죠


잘 가져와지네요!

3. 커스텀 클래스

이번에는 중첩 클래스를 가져오겠습니다. 뭐 Vector와 Color는 Unity에서 지원하며 struct이긴 하지만 상관 없습니다. 그리고 제가 따로 만든 HasClass가 있습니다.

참고로 중첩 클래스가 가지고 있는 필드도 무조건!! [SerializeField] 어트리뷰트를 가지고 있거나, public이여야 합니다. HasClass 내부의 number와 text가 [SerializeField]를 가지고 있는 것처럼요.

그리고 중첩 클래스는 일반 자료형들과 다른 특별한 규칙이 있습니다.
그게 좀 글로 이해하기는 어려우니 일단 데이터 사진부터 보겠습니다.

혹시 위 사진에서 어떤 규칙을 발견하셨나요?
추리 게임을 하는 것도 아니니 굳이 찾으실 필요는 없지만 CsvUtiltity를 사용하고 싶다면 규칙을 알긴 해야 하니 부족한 제 필력으로 짧고 간단하게 설명해보도록 하겠습니다.

규칙 자체는 굉장히 간단합니다.
바로 시작과 끝에 필드 명을 넣습니다. 그리고 그 사이에 클래스 안의 필드를 작성합니다.

Vector3 타입인 vector변수를 보도록 합시다. 우선 처음에 변수명인 vector를 입력합니다. 그리고 Vector3의 필드인 x, y, z를 작성합니다. 그리고 마지막에 다시 변수명을 작성합니다.

나머지도 모두 마찬가지입니다.

저희가 함수 작성할 때 { // code } 형식처럼 중괄호를 이용해 열고 닫지 않습니까? 이것도 그냥 열고 닫는다고 생각하시면 됩니다. 다만 중괄호가 아니라 변수명을 작성하는 거죠.

그럼 코드를 확인해봅시다.

이번에는 Log를 따로 찍어서 확인할 데이터가 없으므로 화살표 함수로 딱 한 줄만에 처리할 수 있습니다. 바로 실행해봅시다.

함수를 실행하기 전 모습입니다. 기본값으로 되어 있습니다.

실행한 후 코드입니다.
Color 필드가 있어서 알록달록하네요!!

4. 이게 되네....?

바로 위에서 유니티에서 정의한 Vector와 Color필드를 로드한 것을 보셨을겁니다.
이 부분에 대해 따로 할 말이 있습니다. 결론부터 솔직하게 말하자면, 노린거 아닙니다.

저는 유저가 따로 정의한 커스텀 클래스만 로드할 생각이였는데 어쩌다보니 유니티가 정의한 것들도 포함이 됐습니다. 말 그대로 얻어걸린 겁니다.

하지만! 얻어걸린게 어떻습니까, 잘 작동하면 되는거지. 그리고 그걸 잘 사용하면 됩니다. 이 부분에 대해서 팁을 드리고자 합니다.

제가 로드하거나 저장할 데이터는 무조건!! [SerializeField]를 들고 있거나 public 이어야 한다고 말했습니다. 그리고 프로퍼티도 안 됩니다.

하지만 Vector에 x, y, z. 그리고, Color에 r, g, b, a는 public 변수였습니다. 저도 그걸 확인하고 한 번 테스트해봤더니 "이게 되네?" 가 된 겁니다. 그럼 이건 어떻게 확인할까요?

간단합니다. 정의를 보는 기능을 Vs에서 제공합니다. 바로 F12(정의로 이동) 입니다.

우선 Vs에 Class타입을 입력합니다.

그리고 위 사진처럼 Vector3에 우클릭을 후 정의로 이동을 누르거나, 더블 클릭으로 강조 후 F12를 누르면 됩니다.


그러면 보시다시피 관련 메타 데이터가 쭉 나오게 됩니다.
필드, 생성자, 프로퍼티, 함수 순으로 나오는데요. 선언만 볼 수 있으며 내부 구현은 나오지 않습니다.
하지만 이것만으로 저희는 x, y, z가 public float라는 것을 알 수 있으며 이를 이용해 CsvUtilitu에 사용할 수 있습니다.

Color도 마찬가지입니다.

Save

Save는 Load에 비해 훨씬 쉽습니다. 자기가 알아서 규칙대로 csv 내용을 만들어주거든요.

우선 저장할 클래스입니다.

그리고 저장할 데이터입니다.

마지막으로 코드입니다.

코드가 역대급으로 길군요. 근데 사실 csv로 변환하는 코드는 한 줄 입니다.

이게 끝입니다. 위에 코드는 딕셔너리를 인스팩터에서 설정할 수 없으니 제가 값을 임의로 넣은 것입니다. 아래에 코드는 파일을 저장하는 코드입니다.
결론적으로 딱 한 줄이면 csv를 얻을 수 있습니다.

이제 실행 결과를 확인해보죠.

잘 저장되었습니다. 하지만 보기만 좀 힘들죠? 이를 위해 옵션으로 배열, 리스트, 딕셔너리가 몇 개의 셀을 사용할 것인지 정할 수 있습니다. 말로 하면 이해하기 힘드니 사진으로 봅시다.

바뀐 코드는 사진에 내용뿐입니다. 로드하는 코드에 인자값으로 사용할 셀의 길이를 주었습니다. 배열, 리스트, 딕셔너리 순입니다.
위 코드는 배열 2칸, 딕셔너리 2칸을 사용하겠다는 뜻입니다.

바로 결과를 봐보죠.

보시면 아까는 한 칸만 사용했던 numbers와 MetacriticScoreByGame이 코드 옵션에서 정의한 대로 2칸씩 사용하고 있는 것을 보실 수 있습니다.

화질이 깨져서 사진을 보기 더 힘들어지긴 했지만, 크기에 맞게 배열, 리스트, 딕셔너리가 사용하는 칸을 늘리면 파일을 관리하시기가 좀 더 편하실 겁니다.

주의할 점

우선 이 에셋을 제가 만들었다 보니 기술적으로 해결하지 못한 부분이 몇개 있습니다. 그 부분을 언급하겠습니다.

1. 지원되지 않는 타입

지원하지 않는 타입은 지원하는 타입을 제외하는 모든 타입들입니다.

byte, int, long, string, float, double, bool, enum과 이를 사용하는 Array, List, Dictionary, class, struct를 지원합니다.

2차원 배열 등의 자료구조는 지원하지 않습니다.

2. 딕셔너리 이용시

딕셔너리를 이용하실 경우 무조건!! new를 통해 할당을 해주셔야 합니다.

Dictionary<T1, T2>() dictionary = new Dictionary<T1, T2>();

이렇게 말이죠. 즉 구조체에서는 딕셔너리를 못 쓴다는 말과 똑같습니다.

전체 소스코드

마지막으로 전체 소스코드를 공유하고 끝내겠습니다. 여기까지 읽어주셔서 감사합니다. 그리고 만약 이 에셋을 사용해주시면 더더 감사합니다.

파일은 총 2개입니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Diagnostics;
using ParserCore;
using Debug = UnityEngine.Debug;

public static class CsvUtility
{
    class CsvSaveOption
    {
        [SerializeField] int _arrayCount;
        [SerializeField] int _listCount;
        [SerializeField] int _dictionaryCount;

        public int ArrayCount => (_arrayCount > 0) ? _arrayCount : 1;
        public int ListCount => (_listCount > 0) ? _listCount : 1;
        public int DitionaryCount => (_dictionaryCount > 0) ? _dictionaryCount : 1;

        public CsvSaveOption(int arrayCount, int listCount = 1, int dictionaryCount = 1)
        {
            _arrayCount = arrayCount;
            _listCount = listCount;
            _dictionaryCount = dictionaryCount;
        }
    }

    
    public static List<T> CsvToList<T>(string csv) => GetEnumerableFromCsv<T>(csv).ToList();
    public static T[] CsvToArray<T>(string csv) => GetEnumerableFromCsv<T>(csv).ToArray();
    static IEnumerable<T> GetEnumerableFromCsv<T>(string csv) => new CsvLoder<T>(csv).GetInstanceIEnumerable();


    public static string ListToCsv<T>(List<T> list, int arrayLength = 1, int listLength = 1, int dictionaryLength = 1)
        => GetSaver<T>(arrayLength, listLength, dictionaryLength).EnumerableToCsv(list);
    public static string ArrayToCsv<T>(T[] array, int arrayLength = 1, int listLength = 1, int dictionaryLength = 1)
        => GetSaver<T>(arrayLength, listLength, dictionaryLength).EnumerableToCsv(array);
    static CsvSaver<T> GetSaver<T>(int arrayLength, int listLength, int dictionaryLength) => new CsvSaver<T>(arrayLength, listLength, dictionaryLength);


    static string SubLastLine(string text) => text.Substring(0, text.Length - 1);
    static IEnumerable<FieldInfo> GetSerializedFields(object obj)
    {
        Type type = obj as Type;
        return type == null ? GetSerializedFields(obj.GetType()) : GetSerializedFields(type);
    }
    static IEnumerable<FieldInfo> GetSerializedFields(Type type)
        => type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
               .Where(x => CsvSerializedCondition(x));

    static bool CsvSerializedCondition(FieldInfo info) => info.IsPublic || info.GetCustomAttribute(typeof(SerializeField)) != null;

    static Type GetElementType(Type type) => type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0];
    static Type GetCoustomType(Type type) => TypeIdentifier.IsIEnumerable(type) ? GetElementType(type) : type;
    static List<string> GetConcatList(List<string> origin, IEnumerable<string> addValue) => origin.Concat(addValue).ToList();
    static List<string> GetSerializedFieldNames(Type type)
    {
        List<string> restul = new List<string>();
        foreach (FieldInfo info in GetSerializedFields(type))
        {
            restul.Add(info.Name);

            if (TypeIdentifier.IsCustom(info.FieldType))
                restul = GetConcatList(restul, GetSerializedFieldNames(GetCoustomType(info.FieldType)));
        }
        return restul;
    }

    static bool IsPrimitive(Type type) => type.IsPrimitive || type == typeof(string) || type.IsEnum 
        || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>));

    class CsvLoder<T>
    {
        const char comma = ',';
        const char mark = '\"';
        const char replaceMark = '+';
        const char lineBreak = '\n';

        string _csv;
        string[] fieldNames;

        string[] GetCells(string line) => line.Split(comma).Select(x => x.Trim()).ToArray();

        List<string> GetValueList(string line)
        {
            string[] tokens = line.Split(mark);

            for (int i = 0; i < tokens.Length - 1; i++)
            {
                if (i % 2 == 1)
                    tokens[i] = tokens[i].Replace(',', replaceMark);
            }
            return GetCells(string.Join("", tokens).Replace("\"", "")).ToList();
        }

        [Conditional("UNITY_EDITOR")]
        void CheckFieldNames(Type type, string[] filedNames)
        {
            List<string> realFieldNames = GetSerializedFieldNames(typeof(T));

            for (int i = 0; i < filedNames.Length; i++)
                Debug.Assert(realFieldNames.Contains(filedNames[i]), $"Unable to find {i + 1}th column name: {filedNames[i]}");
        }

        public CsvLoder(string csv)
        {
            _csv = csv.Substring(0, csv.Length - 1);
            fieldNames = GetCells(_csv.Split(lineBreak)[0]);
            CheckFieldNames(typeof(T), fieldNames);
        }

        Dictionary<string, int> GetCountByFieldName(Type type, string[] fieldNames)
        {
            Dictionary<string, int> result = new Dictionary<string, int>();
            foreach (FieldInfo info in GetSerializedFields(type).Where(x => fieldNames.Contains(x.Name)))
                result.Add(info.Name, GetCount(info));
            return result;

            int GetCount(FieldInfo _info)
            {
                if (IsPrimitive(_info.FieldType))
                    return 1;
                else if (TypeIdentifier.IsCustom(_info.FieldType))
                    return fieldNames.Count(x => x == _info.Name) - 1;
                else if (TypeIdentifier.IsIEnumerable(_info.FieldType))
                    return fieldNames.Count(x => x == _info.Name);

                Debug.LogError($"Unloadable type : {_info.FieldType}, variable name : {_info.Name}, class type : {type}");
                return 1;
            }
        }

        public IEnumerable<T> GetInstanceIEnumerable() => _csv.Split(lineBreak).Skip(1).Select(x => (T)GetInstance(typeof(T), fieldNames, GetValueList(x)));

        object GetInstance(Type type, string[] fieldNames, List<string> cells)
        {
            object obj = Activator.CreateInstance(type);
            Dictionary<string, int> countByKey = GetCountByFieldName(type, fieldNames);

            foreach (FieldInfo info in GetSerializedFields(type).Where(x => fieldNames.Contains(x.Name)))
            {
                if (TypeIdentifier.IsCustom(info.FieldType))
                {
                    info.SetValue(obj, GetCustomValue(info, cells));
                    cells.RemoveAt(0);
                }
                else
                    CsvParsers.GetParser(info).SetValue(obj, info, GetFieldValues(countByKey[info.Name], cells));
            }
            return obj;
        }

        object GetCustomValue(FieldInfo _info, List<string> cells)
        {
            if (TypeIdentifier.IsIEnumerable(_info.FieldType))
                return GetCustomArray(_info, cells);
            else
                return GetSingleCustomValue(_info.FieldType, cells, _info.Name);
        }

        object GetSingleCustomValue(Type type, List<string> cells, string name, int index = 0)
        {
            cells.RemoveAt(0);
            if (GetFieldValues(GetSerializedFields(type).Count(), new List<string>(cells)).Where(x => string.IsNullOrEmpty(x) == false).Count() == 0)
            {
                GetFieldValues(GetSerializedFields(type).Count(), cells);
                return null;
            }
            else
                return GetInstance(GetCoustomType(type), GetCustomFieldNames(index), cells);

            string[] GetCustomFieldNames(int startIndex)
            {
                int[] indexs = fieldNames.Select((value, index) => new { value, index }).Where(x => x.value == name).Select(x => x.index).ToArray();
                return fieldNames.Skip(indexs[startIndex] + 1).Take(indexs[startIndex + 1] - indexs[startIndex] - 1).ToArray();
            }
        }

        object GetCustomArray(FieldInfo info, List<string> cells)
        {
            int length = fieldNames.Where(x => x == info.Name).Count() - 1;
            Type elementType = GetElementType(info.FieldType);

            List<object> objs = new List<object>();
            for (int i = 0; i < length; i++)
            {
                var value = GetSingleCustomValue(elementType, cells, info.Name, i);
                if (value != null)
                    objs.Add(value);
            }

            if (objs.Count == 0) return null;

            Array array = Array.CreateInstance(elementType, objs.Count);
            for (int i = 0; i < objs.Count; i++)
                array.SetValue(objs[i], i);

            return GetIEnumerableValue(info.FieldType, array);
        }
        object GetIEnumerableValue(Type type, Array array) => (type.IsArray) ? array : type.GetConstructors()[2].Invoke(new object[] { array });


        string[] GetFieldValues(int count, List<string> cells)
        {
            List<string> result = new List<string>();

            for (int i = 0; i < count; i++)
            {
                string value = cells[0];
                if (string.IsNullOrEmpty(value) == false)
                    result.Add(value);
                cells.RemoveAt(0);
            }

            if (result.Count == 0) result.Add("");
            return result.ToArray();
        }
    }

    class CsvSaver<T>
    {
        CsvSaveOption _option;

        Dictionary<Type, int> _customCountByType;

        Dictionary<Type, int> GetCountByType(IEnumerable<T> datas)
        {
            Dictionary<Type, int> countByType = new Dictionary<Type, int>();
            foreach (T data in datas)
            {
                foreach (FieldInfo info in GetSerializedFields(data).Where(x => TypeIdentifier.IsCustom(x.FieldType) && TypeIdentifier.IsIEnumerable(x.FieldType)))
                {
                    if (countByType.ContainsKey(info.FieldType) == false)
                        countByType.Add(info.FieldType, 1);

                    int count = 0;
                    foreach (var item in info.GetValue(data) as IEnumerable)
                        count++;
                    if (count > countByType[info.FieldType])
                        countByType[info.FieldType] = count;
                }
            }
            return countByType;
        }

        int GetTypeCellCount(Type type)
        {
            int result = 0;
            foreach (FieldInfo info in GetSerializedFields(type))
                result += GetOptionCount(info.FieldType);
            return result;
        }

        public CsvSaver(int arrayLength, int listLength, int dictionaryLength)
        {
            _option = new CsvSaveOption(arrayLength, listLength, dictionaryLength);
            _customCountByType = new Dictionary<Type, int>();
            _customCountByType.Clear();
        }

        int GetOptionCount(Type type)
        {
            if (TypeIdentifier.IsIEnumerable(type) == false) return 1;
            else if (type.IsArray) return _option.ArrayCount;
            else if (TypeIdentifier.IsList(type)) return _option.ListCount;
            else if (TypeIdentifier.IsDictionary(type)) return _option.DitionaryCount;
            else
            {
                Debug.LogWarning("정의할 수 없는 타입");
                return 1;
            }
        }

        public string EnumerableToCsv(IEnumerable<T> datas)
        {
            StringBuilder stringBuilder = new StringBuilder();
            _customCountByType = GetCountByType(datas);
            stringBuilder.AppendLine(string.Join(",", GetFirstRow(typeof(T), _customCountByType)));

            foreach (var data in datas)
            {
                IEnumerable<string> values = GetValues(data);
                stringBuilder.AppendLine(string.Join(",", values));
            }

            return SubLastLine(stringBuilder.ToString());
        }

        List<string> GetFirstRow(object type, Dictionary<Type, int> countByType)
        {
            List<string> result = new List<string>();
            foreach (FieldInfo info in GetSerializedFields(type))
            {
                if (TypeIdentifier.IsCustom(info.FieldType))
                {
                    result.Add(info.Name);
                    if (TypeIdentifier.IsIEnumerable(info.FieldType))
                        result = GetCustomConcat(result, info, countByType);
                    else
                        result = GetCustomList(result, () => GetFirstRow(info.FieldType, countByType), info.Name);
                }
                else
                {
                    for (int i = 0; i < GetOptionCount(info.FieldType); i++)
                        result.Add(info.Name);
                }
            }
            return result;
        }

        List<string> GetCustomConcat(List<string> result, FieldInfo info, Dictionary<Type, int> countByType)
        {
            for (int i = 0; i < countByType[info.FieldType]; i++)
                result = GetCustomList(result, () => GetFirstRow(GetElementType(info.FieldType), countByType), info.Name);
            return result;
        }


        IEnumerable<string> GetValues(object data)
        {
            List<string> result = new List<string>();
            foreach (FieldInfo info in GetSerializedFields(data.GetType()))
            {
                if (IsPrimitive(info.FieldType))
                    result.Add(info.GetValue(data).ToString());
                else if (TypeIdentifier.IsCustom(info.FieldType))
                    result = GetCustomConcat(data, result, info);
                else if (TypeIdentifier.IsIEnumerable(info.FieldType))
                {
                    result = 
                        GetConcatList(result, GetIEnumerableValue(GetOptionCount(info.FieldType), new EnumerableTypeParser().GetIEnumerableValues(data, info)));
                }
            }
            return result;
        }
        string[] GetIEnumerableValue(int count, string[] values)
        {
            string[] result = new string[count];
            int[] counts = GetCounts(count, values.Length);
            int current = 0;
            for (int i = 0; i < count; i++)
            {
                result[i] = GetValue(values.Skip(current).Take(counts[i]));
                current += counts[i];
            }

            return result;
        }

        int[] GetCounts(int count, int valueLength)
        {
            int length = valueLength;
            int[] counts = new int[count];
            while (length > 0)
            {
                for (int i = 0; i < count; i++)
                {
                    counts[i]++;
                    length--;
                    if (length <= 0) break;
                }
            }

            return counts;
        }

        string GetValue(IEnumerable<string> values)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("\"");
            stringBuilder.Append(string.Join(",", values));
            stringBuilder.Append("\"");
            return stringBuilder.ToString();
        }

        List<string> GetCustomConcat(object data, List<string> result, FieldInfo info)
        {
            result.Add("");
            if (TypeIdentifier.IsIEnumerable(info.FieldType))
            {
                int count = _customCountByType[info.FieldType];
                foreach (var item in info.GetValue(data) as IEnumerable)
                {
                    result = GetCustomList(result, () => GetValues(item));
                    count--;
                }
                for (int i = 0; i < count; i++)
                {
                    for (int j = 0; j < GetTypeCellCount(GetElementType(info.FieldType)) + 1; j++)
                        result.Add("");
                }
            }
            else
                result = GetCustomList(result, () => GetValues(info.GetValue(data)));

            return result;
        }

        List<string> GetCustomList(List<string> result, Func<IEnumerable<string>> OriginFunc, string blank = "")
        {
            result = GetConcatList(result, OriginFunc());
            result.Add(blank);
            return result;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Reflection;
using System;
using System.Linq;
using System.Text;

namespace ParserCore
{
    public class CsvParsers
    {
        public static CsvParser GetParser(FieldInfo info)
        {
            if (TypeIdentifier.IsIEnumerable(info.FieldType))
                return new EnumerableTypeParser();
            else
                return new PrimitiveTypeParser();
        }
    }

    enum EnumerableType
    {
        Unknown,
        Array,
        List,
        Dictionary,
    }

    public interface ICsvIEnumeralbeParser
    {
        string[] GetCsvValues(object obj, FieldInfo info);
    }

    public interface CsvPrimitiveTypeParser
    {
        object GetParserValue(string value);
        Type GetParserType();
    }

    public interface CsvParser
    {
        void SetValue(object obj, FieldInfo info, string[] values);
    }

    class PrimitiveTypeParser : CsvParser
    {
        public static CsvPrimitiveTypeParser GetPrimitiveParser(Type type)
        {
            if (type == typeof(int)) return new CsvIntParser();
            else if (type == typeof(byte)) return new CsvByteParser();
            else if (type == typeof(long)) return new CsvLongParser();
            else if (type == typeof(string)) return new CsvStringParser();
            else if (type == typeof(float)) return new CsvFloatParser();
            else if (type == typeof(double)) return new CsvDoubleParser();
            else if (type == typeof(bool)) return new CsvBooleanParser();
            else if (type.IsEnum) return new CsvEnumParser(type);
            else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) return new CsvPairParser(type);
            else Debug.LogError($"Unloadable type : {type}");
            return null;
        }

        object GetParserValue(Type type, string value) => GetPrimitiveParser(type).GetParserValue(value);
        public void SetValue(object obj, FieldInfo info, string[] value) => info.SetValue(obj, GetParserValue(info.FieldType, value[0]));
    }

    class EnumerableTypeParser : CsvParser
    {
        EnumerableType GetEnumableType(Type type)
        {
            if (type.IsArray) return EnumerableType.Array;
            else if (TypeIdentifier.IsList(type)) return EnumerableType.List;
            else if (TypeIdentifier.IsDictionary(type)) return EnumerableType.Dictionary;
            return EnumerableType.Unknown;
        }

        public void SetValue(object obj, FieldInfo info, string[] values)
        {
            values = SetValue(values);
            switch (GetEnumableType(info.FieldType))
            {
                case EnumerableType.Array: new CsvArrayParser().SetValue(obj, info, values); break;
                case EnumerableType.List: new CsvListParser().SetValue(obj, info, values); break;
                case EnumerableType.Dictionary: new CsvDictionaryParser().SetValue(obj, info, values); break;
            }
        }

        string[] SetValue(string[] values)
        {
            const char replaceMark = '+';
            StringBuilder builder = new StringBuilder();

            foreach (string value in values.Where(x => !string.IsNullOrEmpty(x)))
            {
                builder.Append(replaceMark);
                builder.Append(value);
            }
            return builder.ToString().Split(replaceMark).Skip(1).Select(x => x.Trim()).ToArray();
        }

        public ICsvIEnumeralbeParser GetIEnumerableParser(Type type)
        {
            if (type.IsArray) return new CsvArrayParser();
            else if (TypeIdentifier.IsList(type)) return new CsvListParser();
            else if (TypeIdentifier.IsDictionary(type)) return new CsvDictionaryParser();
            return null;
        }

        public string[] GetIEnumerableValues(object obj, FieldInfo info)
        {
            ICsvIEnumeralbeParser parser = GetIEnumerableParser(info.FieldType);
            if (parser != null)
                return parser.GetCsvValues(obj, info);
            else
                return null;
        }
    }

    #region 기본형 파싱

    class CsvByteParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value)
        {
            Byte.TryParse(value, out byte result);
            return result;
        }

        public Type GetParserType() => typeof(byte);
    }

    class CsvIntParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value)
        {
            Int32.TryParse(value, out int valueInt);
            return valueInt;
        }

        public Type GetParserType() => typeof(int);
    }

    class CsvLongParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value)
        {
            long.TryParse(value, out long valueInt);
            return valueInt;
        }

        public Type GetParserType() => typeof(long);
    }

    class CsvFloatParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value)
        {
            float.TryParse(value, out float valueFloat);
            return valueFloat;
        }

        public Type GetParserType() => typeof(float);
    }

    class CsvDoubleParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value)
        {
            double.TryParse(value, out double valueFloat);
            return valueFloat;
        }

        public Type GetParserType() => typeof(double);
    }

    class CsvStringParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value) => value;
        public Type GetParserType() => typeof(string);
    }

    class CsvBooleanParser : CsvPrimitiveTypeParser
    {
        public object GetParserValue(string value) => value == "True" || value == "TRUE" || value == "true";
        public Type GetParserType() => typeof(bool);
    }

    class CsvEnumParser : CsvPrimitiveTypeParser
    {
        Type _type;
        public CsvEnumParser(Type type)
        {
            _type = type;
        }

        public object GetParserValue(string value)
        {
            object result = null;
            try
            {
                result = Enum.Parse(_type, value);
            }
            catch
            {
                Debug.LogError($"CsvUtility Message : The requested value {value} was not found within {_type} enum.");
            }
            return result;
        }
        public Type GetParserType() => _type;
    }

    #endregion 기본형 파싱 End




    #region 열거형 파싱
    public class CsvListParser : ICsvIEnumeralbeParser
    {
        public void SetValue(object obj, FieldInfo info, string[] values)
        {
            ConstructorInfo constructor = info.FieldType.GetConstructors()[2];
            info.SetValue(obj, constructor.Invoke(new object[] { GetValue(info, values) }));
        }

        Array GetValue(FieldInfo info, string[] values)
            => new CsvArrayParser().GetValue(info.FieldType.GetGenericArguments()[0], values);

        public string[] GetCsvValues(object obj, FieldInfo info)
        {
            IList list = info.GetValue(obj) as IList;
            List<string> result = new List<string>();
            foreach (var item in list) result.Add(item.ToString());
            return result.ToArray();
        }
    }

    public class CsvArrayParser : ICsvIEnumeralbeParser
    {
        public void SetValue(object obj, FieldInfo info, string[] values)
            => info.SetValue(obj, GetValue(info.FieldType.GetElementType(), values));

        public Array GetValue(Type elementType, string[] values)
        {
            Array array = Array.CreateInstance(PrimitiveTypeParser.GetPrimitiveParser(elementType).GetParserType(), values.Length);
            for (int i = 0; i < array.Length; i++)
                array.SetValue(PrimitiveTypeParser.GetPrimitiveParser(elementType).GetParserValue(values[i]), i);
            return array;
        }

        public string[] GetCsvValues(object obj, FieldInfo info)
        {
            Array array = info.GetValue(obj) as Array;
            List<string> result = new List<string>();
            foreach (var item in array) result.Add(item.ToString());
            return result.ToArray();
        }
    }

    public class CsvDictionaryParser : ICsvIEnumeralbeParser
    {
        public void SetValue(object obj, FieldInfo info, string[] values)
        {
            if (values.Length % 2 != 0) Debug.LogError($"{info.Name} : The input is incorrect.Please make sure you entered the Dictionary correctly.");

            Type[] elementTypes = info.FieldType.GetGenericArguments();
            MethodInfo methodInfo = info.FieldType.GetMethod("Add");
            for (int i = 0; i < values.Length; i += 2)
            {
                methodInfo.Invoke(info.GetValue(obj), new object[] { PrimitiveTypeParser.GetPrimitiveParser(elementTypes[0]).GetParserValue(values[i]),
                                                                 PrimitiveTypeParser.GetPrimitiveParser(elementTypes[1]).GetParserValue(values[i+1]) });
            }
        }

        public string[] GetCsvValues(object obj, FieldInfo info)
        {
            IDictionary dictionary = info.GetValue(obj) as IDictionary;
            List<string> keys = new List<string>();
            List<string> values = new List<string>();

            foreach (var item in dictionary.Keys) keys.Add(item.ToString());
            foreach (var item in dictionary.Values) values.Add(item.ToString());

            List<string> result = new List<string>();
            for (int i = 0; i < keys.Count; i++)
            {
                result.Add(keys[i]);
                result.Add(values[i]);
            }

            return result.ToArray();
        }
    }
    #endregion 열거형 파싱 End

    public class CsvPairParser : CsvPrimitiveTypeParser
    {
        Type _type;
        public CsvPairParser(Type type)
        {
            _type = type;
        }

        public IEnumerable GetParserEnumerable(string[] values)
        {
            throw new NotImplementedException();
        }

        public Type GetParserType()
        {
            throw new NotImplementedException();
        }

        public object GetParserValue(string value)
        {
            string[] values = value.Split('+');
            if (values.Length != 2) Debug.LogError($"{_type} : The input is incorrect.Please make sure you entered the Key Value pair correctly.");
            Type[] elementTypes = _type.GetGenericArguments();
            ConstructorInfo constructor = _type.GetConstructors()[0];
            return constructor.Invoke(new object[] { PrimitiveTypeParser.GetPrimitiveParser(elementTypes[0]).GetParserValue(values[0]),
                                                             PrimitiveTypeParser.GetPrimitiveParser(elementTypes[1]).GetParserValue(values[1]) });
        }
    }



    public static class TypeIdentifier
    {
        public static bool IsList(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
        public static bool IsDictionary(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>);
        public static bool IsIEnumerable(Type type) => type.IsArray || IsList(type) || IsDictionary(type);
        public static bool IsCustom(Type type)
        {
            if (type.IsEnum) return false;
            else if (type.ToString().StartsWith("System.")) return false;
            else if (type.IsArray) return IsCustom(type.GetElementType());
            else if (IsList(type) && type.GetGenericArguments()[0] != null) return IsCustom(type.GetGenericArguments()[0]);
            else return true;
        }
    }
}
profile
수강신청 망친 새내기 개발자

0개의 댓글