State Machine 기반의 로직을 작성할 때

정답이라고 할 수는 없지만 개인적인 생각에는

class StateMachine {
  std::list<StatePtr> stateStack;
public:
  StateMachine(State* baseState) {
    stateStack.push_back(baseState);
  }

  void Push(StatePtr newState) {
    stateStack.front()->Pause();
    stateStack.push_front(newState);
    stateStack.front()->Start();
  }

  void Change(StatePtr newState) {
    if (stateStack.size() <= 1)
      return Push(newState);
    stateStack.front()->Stop();
    stateStack.pop_front();
    stateStack.push_front(newState);
    stateStack.front()->Start();
  }

  void Pop() {
    if (stateStack.size() <= 1)
      return;
    stateStack.front()->Stop();
    stateStack.pop_front();
    stateStack.front()->Resume();
  }

  void Reset() {
    while (stateStack.size() > 1) {
      stateStack.front()->Stop();
      stateStack.pop_front();
    }
  }

  void SendMessage(Message& msg) {
    for (auto& state : stateStack) {
      if (state->OnMessage(msg))
        return;
    }
  }
};

위와 같이 스택 구조로 만들면… 이전 상태가 기록되어 좋다고 생각합니다. 뭐 이런 느낌???

// 선공, 비선공으로 NPC AI 를 시작
aiStateMachine.Push(offensive ? new OffensiveState(this) : new PeacefulState(this));
// 어떤 조건으로 인해 전투 상태로 변경
aiStateMachine.Push(new CombatState(this));
// 다른 조건으로 NPC가 스킬을 시전
aiStateMachine.Push(new UseSkillState(this));

// 스킬 사용 상태
class UseSkillState : public State {
public:
  virtual bool OnMessage(Message& msg) override {
    switch (msg.GetType()) {
    case MessageType::GET_TARGET:
      // 이전 상태에서 구한다
      return false;
    // ...
    }
  }

  virtual void Start() override {
    // 대상을 구한다
    GetTargetMessage msg;
    owner->GetStateMachine().SendMessage(msg);
    if (!msg.HasTarget()) return owner->GetStateMachine().Pop(); // 대상을 못 구했으므로 종료
    // 대상에게 스킬 시전
    // ...
  }
}

사진 섬네일 만들기

간단하게 구현해 보았습니다.

주요 부분을 설명하자면

// .NET Framework 에서 제공하는 Built-in JPEG 인코더를 구해서
var jpegEncoder = ImageCodecInfo.GetImageEncoders().Where(info => info.FormatID == ImageFormat.Jpeg.Guid).FirstOrDefault();
// 인코딩 파라미터로 화질을 결정하고
var encoderParams = new EncoderParameters(1);
var encoderParam = new EncoderParameter(Encoder.Quality, 50L);
encoderParams.Param[0] = encoderParam;
// 원본을 읽어서
var srcImage = new Bitmap(@"C:\Photo\SourceFile.JPG");
// 리사이징 한 후에
var newImage = new Bitmap(srcImage, newSize);
// 원본의 EXIF 정보를 그대로 옮기고
foreach (var propItem in srcImage.PropertyItems)
  newImage.SetPropertyItem(propItem);
// 저장 합니다
newImage.Save(@"C:\Photo\Thumbnail.JPG", jpegEncoder, encoderParams);
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace BSPFP {
  class BSException : Exception {
    public BSException() : base() { }
    public BSException(string msg) : base(msg) { }
    public BSException(string fmt, params object[] args) : base(string.Format(fmt, args)) { }
  }

  class Program {
    static readonly int longSideLength = 640; // 섬네일의 긴쪽 길이
    static readonly long quality = 50L; // 섬네일 화질 (0 - 100)
    static readonly HashSet<string> srcFolders = new HashSet<string> {
      Path.GetFullPath(Path.Combine(@"C:\Photo\사진 원본 1", ".")),
      Path.GetFullPath(Path.Combine(@"C:\Photo\사진 원본 2", ".")),
    };
    static readonly string destFolder = Path.GetFullPath(Path.Combine(@"C:\Photo\사진 섬네일", "."));
    static readonly HashSet<string> targetExt = new HashSet<string> { ".jpeg", ".jpg" };

    static readonly int concurrentCount = Environment.ProcessorCount;

    static void CheckReadOnlyVariables() {
      foreach (var srcFolder in srcFolders) {
        if (string.Compare(srcFolder, destFolder, true) == 0)
          throw new BSException("원본, 대상 폴더가 같음: {0}: {1}", srcFolder, destFolder);
        if (destFolder.ToLower().StartsWith(srcFolder.ToLower()))
          throw new BSException("대상이 원본 하위 폴더: {0}: {1}", srcFolder, destFolder);
      }
    }

    static ConcurrentQueue<Tuple<string, string>> CollectSourceFiles() {
      var ret = new ConcurrentQueue<Tuple<string, string>>();
      foreach (var srcFolder in srcFolders) {
        var folderQueue = new Queue<string>();
        folderQueue.Enqueue(srcFolder);
        while (folderQueue.Count > 0) {
          var f = folderQueue.Dequeue();
          foreach (var e in Directory.EnumerateFileSystemEntries(f)) {
            var fi = new FileInfo(e);
            if ((fi.Attributes & FileAttributes.Directory) != 0) {
              folderQueue.Enqueue(e);
              continue;
            }
            if (!targetExt.Contains(fi.Extension.ToLower()))
              continue;
            ret.Enqueue(new Tuple<string, string>(srcFolder, fi.FullName));
          }
        }
      }
      return ret;
    }

    static int Main() {
      try {
        CheckReadOnlyVariables();
        var srcFiles = CollectSourceFiles();

        var tasks = new Task[concurrentCount];
        for (var i = 0; i < concurrentCount; i++) { tasks[i] = Task.Factory.StartNew(() => {
            var jpegEncoder = ImageCodecInfo.GetImageEncoders().Where(info => info.FormatID == ImageFormat.Jpeg.Guid).FirstOrDefault();
            using (var encoderParams = new EncoderParameters(1))
            using (var encoderParam = new EncoderParameter(Encoder.Quality, quality)) {
              encoderParams.Param[0] = encoderParam;
              while (srcFiles.TryDequeue(out Tuple<string, string> src)) {
                using (var srcImage = new Bitmap(src.Item2)) {
                  Size newSize;
                  if (srcImage.Size.Width >= srcImage.Size.Height)
                    newSize = new Size(longSideLength, (int)((double)srcImage.Size.Height * longSideLength / srcImage.Size.Width));
                  else
                    newSize = new Size((int)((double)srcImage.Size.Width * longSideLength / srcImage.Size.Height), longSideLength);
                  using (var newImage = new Bitmap(srcImage, newSize)) {
                    foreach (var propItem in srcImage.PropertyItems)
                      newImage.SetPropertyItem(propItem);
                    var subPath = src.Item2.Substring(src.Item1.Length + 1);
                    var folder = Path.GetDirectoryName(Path.GetFullPath(Path.Combine(destFolder, subPath)));
                    var filename = Path.GetFileNameWithoutExtension(src.Item2);
                    var destFile = Path.Combine(folder, filename + ".jpg");
                    if (!Directory.Exists(folder))
                      Directory.CreateDirectory(folder);
                    Console.WriteLine(subPath);
                    newImage.Save(destFile, jpegEncoder, encoderParams);
                  }
                }
              }
            }
          });
        }

        Task.WaitAll(tasks);
        for (var i = 0; i < concurrentCount; i++)
          tasks[i].Dispose();
      } catch (Exception ex) {
        Console.WriteLine(ex.Message);
        Console.WriteLine(ex.StackTrace);
        return 1;
      }
      return 0;
    }
  }
}

HTML5 + CSS + JavaScript 로 멀티 플랫폼 Application 을 만들기

Desktop 용 Application 을 만든다면 [Electron]
예) Skype, Visual Studio Code, Atom, Slack, …

Mobile 용 App 을 만든다면 [Apache Cordova]
Cordova 로 직접 App 을 만드는 건 마치 Java 로 Andriod 앱을 만드는 것과 다름이 없을 거 같고…
Cordova 를 다시 특정 용도에 맞게 좀더 다듬은 것을 사용하지 않을까 생각되네요.
예) Adobe PhoneGap, Ionnic, InstaBug, …

HyperLogLog 에 대해 잠깐 테스트를 해 보았습니다.

BS가 근무하는 팀에서 이런 저런 스터디를 하는데 Redis 에 대해서 스터디 중입니다.
이번 스터디에 Redis 의 PFADD, PFCOUNT, PFMERGE 를 다루게 되어 조금 찾아 보았습니다.

참고자료 #1: [성태의 닷넷 이야기]
참고자료 #2: [Naver D2]
참고자료 #3: [Adnan.Korkmaz Blog]
참고자료 #4: [Microsoft CardinalityEstimation]
참고자료 #5: [Wikipedia]

위 자료에서 3, 4번의 소스와 라이브러리를 받아 간단하게 테스트 프로그램을 작성하였습니다.

using CardinalityEstimation;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace HyperLogLogTest {

    // from http://adnan-korkmaz.blogspot.com/2012/06/hyperloglog-c-implementation.html

    public class HyperLogLog {
        private readonly double mapSize, alpha_m, k;
        private readonly int kComplement;
        private readonly Dictionary<int, int> Lookup = new Dictionary<int, int>();
        private const double pow_2_32 = 4294967297;
        private readonly Func<object, uint> hashFunc;

        public HyperLogLog(double stdError, Func<object, uint> hashFunc) {
            this.hashFunc = hashFunc;

            mapSize = 1.04 / stdError;
            k = (long)Math.Ceiling(Log2(mapSize * mapSize));

            kComplement = 32 - (int)k;
            mapSize = (long)Math.Pow(2, k);

            alpha_m = mapSize == 16 ? 0.673
                  : mapSize == 32 ? 0.697
                  : mapSize == 64 ? 0.709
                  : 0.7213 / (1 + 1.079 / mapSize);
            for (int i = 0; i < mapSize; i++)
                Lookup[i] = 0;
        }

        private static double Log2(double x) {
            return Math.Log(x) / 0.69314718055994530941723212145818;//Ln2 
        }
        private static int GetRank(uint hash, int max) {
            int r = 1;
            uint one = 1;
            while ((hash & one) == 0 && r <= max) { ++r; hash >>= 1;
            }
            return r;
        }

        public int Count() {
            double c = 0, E;

            for (var i = 0; i < mapSize; i++)
                c += 1d / Math.Pow(2, Lookup[i]);

            E = alpha_m * mapSize * mapSize / c;

            // Make corrections & smoothen things. 
            if (E <= (5 / 2) * mapSize) {
                double V = 0;
                for (var i = 0; i < mapSize; i++) if (Lookup[i] == 0) V++; if (V > 0)
                    E = mapSize * Math.Log(mapSize / V);
            }
            else
                if (E > (1 / 30) * pow_2_32)
                E = -pow_2_32 * Math.Log(1 - E / pow_2_32);
            // Made corrections & smoothen things, or not. 

            return (int)E;
        }

        public void Add(object val) {
            uint hashCode = hashFunc(val);
            int j = (int)(hashCode >> kComplement);

            Lookup[j] = Math.Max(Lookup[j], GetRank(hashCode, kComplement));
        }
    }

    class Program {

        static MD5 md5 = MD5.Create();
        static readonly string outputFormat = "{0,-40}: {1,10}: {2,10}";

        static uint CustomHash(object val) {
            string text = val.ToString();
            uint hash = 0;

            for (int i = 0, l = text.Length; i < l; i++) {
                hash += text[i];
                hash += hash << 10; hash ^= hash >> 6;
            }
            hash += hash << 3; hash ^= hash >> 6;
            hash += hash << 16;

            return hash;
        }

        static uint DefaultHash(object val) {
            return (uint)val.GetHashCode();
        }

        static uint MD5Hash(object val) {
            var bt = md5.ComputeHash(Encoding.UTF8.GetBytes(val.ToString()));
            return BitConverter.ToUInt32(bt, 0) ^ BitConverter.ToUInt32(bt, 4) ^ BitConverter.ToUInt32(bt, 8) ^ BitConverter.ToUInt32(bt, 12);
        }

        static void HyperLogLogTest(Func<object, uint> hashFunc, string name, int testCount) {
            var hll = new HyperLogLog(0.01, hashFunc);
            for (var i = 0; i < testCount; i++)
                hll.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hll.Count());
        }

        // from https://github.com/Microsoft/CardinalityEstimation
        static void MSHyperLogLogTestFnv1A(Func<object, uint> hashFunc, string name, int testCount) {
            var hllMS = new CardinalityEstimator(hashFunctionId: CardinalityEstimation.Hash.HashFunctionId.Fnv1A);
            for (var i = 0; i < testCount; i++)
                hllMS.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hllMS.Count());
        }

        // from https://github.com/Microsoft/CardinalityEstimation
        static void MSHyperLogLogTestMurmur3(Func<object, uint> hashFunc, string name, int testCount) {
            var hllMS = new CardinalityEstimator(hashFunctionId: CardinalityEstimation.Hash.HashFunctionId.Murmur3);
            for (var i = 0; i < testCount; i++)
                hllMS.Add(i);
            Console.WriteLine(outputFormat, name, testCount, hllMS.Count());
        }

        static void Main(string[] args) {
            HyperLogLogTest(DefaultHash, "Adnan.Korkmaz: DefaultHash", 10);
            HyperLogLogTest(DefaultHash, "Adnan.Korkmaz: DefaultHash", 10000);
            HyperLogLogTest(MD5Hash, "Adnan.Korkmaz: MD5 + XOR", 10);
            HyperLogLogTest(MD5Hash, "Adnan.Korkmaz: MD5 + XOR", 10000);
            HyperLogLogTest(CustomHash, "Adnan.Korkmaz: Adnan.Korkmaz", 10);
            HyperLogLogTest(CustomHash, "Adnan.Korkmaz: Adnan.Korkmaz", 10000);
            MSHyperLogLogTestFnv1A(CustomHash, "CardinalityEstimation with Fnv1A", 10);
            MSHyperLogLogTestFnv1A(CustomHash, "CardinalityEstimation with Fnv1A", 10000);
            MSHyperLogLogTestMurmur3(CustomHash, "CardinalityEstimation with Murmur3", 10);
            MSHyperLogLogTestMurmur3(CustomHash, "CardinalityEstimation with Murmur3", 10000);
        }
    }
}

최대한 비슷한 결과를 얻기 위해 두 라이브러리 모두 레지스터 수량을 16K 개(pow(2, 14)) 로 하였습니다.
그리고 결과는 아래와 같이 나왔습니다.

Adnan.Korkmaz: DefaultHash              :         10:          1
Adnan.Korkmaz: DefaultHash              :      10000:          1
Adnan.Korkmaz: MD5 + XOR                :         10:         10
Adnan.Korkmaz: MD5 + XOR                :      10000:      10068
Adnan.Korkmaz: Adnan.Korkmaz            :         10:         10
Adnan.Korkmaz: Adnan.Korkmaz            :      10000:       9853
CardinalityEstimation with Fnv1A        :         10:         10
CardinalityEstimation with Fnv1A        :      10000:      11419
CardinalityEstimation with Murmur3      :         10:         10
CardinalityEstimation with Murmur3      :      10000:       9980

여기에서 확인되는 것은 동일한 알고리즘으로 구현된 것이라도 어떤 Hash 함수를 넣는가에 따라서 굉장히 다른 결과를 얻게 된다는 것입니다.
C#의 GetHashCode() 의 경우에는… 쩝…

Redis 의 PFADD, PFCOUNT 를 쓰면

var redis = require('redis');
var client = redis.createClient();

function test(n) {
  client.flushall();
  for (i = 0; i < n; i++)
    client.pfadd("hll", i);
  client.pfcount("hll", function(e, r) {
    console.log(n, r);
  });
}

test(10);
test(10000);

client.flushall();
client.quit();
10 10
10000 9999

이렇게 나오네요.

metadata-extractor – EXIF 정보를 읽을 수 있는 .NET Framework 용 라이브러리

EXIF 정보를 읽어야 할 일이 생겨서 찾다보니 이것!

metadata-extractor

EXIF란 Exchangeable Image File Format 의 약어로, JPEG, TIFF 등 이미지 파일에 각종 메타정보를 저장하는 포맷입니다.

쌩으로 .NET Framework 에 있는 기본 라이브러리로 읽을 수도 있겠지만… 엄청 고난의 길…

metadata-extractor 는 Java, C# 을 지원하기 때문에 웹 기반 + 사진 관련된 어플리케이션을 개발한다면 유용하게 사용할 수 있습니다.

물론 정말 하드코어하게

Image.PropertyItems 으로 이런 식으로 직접 만들 수 있습니다.

LiteDB – Embedded NoSQL for .NET

.NET Framework 환경에서

  • Embedded
  • NoSQL
  • Single file

로 하면 이것 말고 쓸 게 없네요.

http://www.litedb.org/

SQLite와 비교하면 파일의 용량이 거의 2배가 되긴 하지만요…

순수 C#으로 만들어져 모바일 환경에서도 쓸 수 있습니다.
모습은 음… MongoDB 를 보는 듯한…