using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Threading; namespace POSV.Printer { /// Represents an ObjectId /// [Serializable] public struct DesignerObjectId : IComparable, IEquatable { // private static fields private static readonly DateTime UnixEpoch; private static readonly DesignerObjectId EmptyInstance = default(DesignerObjectId); private static readonly int StaticMachine; private static readonly short StaticPid; private static int _staticIncrement; // high byte will be masked out when generating new ObjectId private static readonly uint[] Lookup32 = Enumerable.Range(0 , 256).Select(i => { string s = i.ToString("x2"); return ((uint)s[0]) + ((uint)s[1] << 16); }).ToArray(); // we're using 14 bytes instead of 12 to hold the ObjectId in memory but unlike a byte[] there is no additional object on the heap // the extra two bytes are not visible to anyone outside of this class and they buy us considerable simplification // an additional advantage of this representation is that it will serialize to JSON without any 64 bit overflow problems private readonly int _timestamp; private readonly int _machine; private readonly short _pid; private readonly int _increment; // static constructor static DesignerObjectId() { UnixEpoch = new DateTime(1970 , 1 , 1 , 0 , 0 , 0 , DateTimeKind.Utc); StaticMachine = GetMachineHash(); _staticIncrement = (new Random()).Next(); StaticPid = (short)GetCurrentProcessId(); } // constructors /// /// Initializes a new instance of the ObjectId class. /// /// The bytes. public DesignerObjectId(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } Unpack(bytes , out _timestamp , out _machine , out _pid , out _increment); } /// /// Initializes a new instance of the ObjectId class. /// /// The timestamp (expressed as a DateTime). /// The machine hash. /// The PID. /// The increment. public DesignerObjectId(DateTime timestamp , int machine , short pid , int increment) : this(GetTimestampFromDateTime(timestamp) , machine , pid , increment) { } /// /// Initializes a new instance of the ObjectId class. /// /// The timestamp. /// The machine hash. /// The PID. /// The increment. public DesignerObjectId(int timestamp , int machine , short pid , int increment) { if ((machine & 0xff000000) != 0) { throw new ArgumentOutOfRangeException(nameof(machine) , "The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); } if ((increment & 0xff000000) != 0) { throw new ArgumentOutOfRangeException(nameof(increment) , "The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); } _timestamp = timestamp; _machine = machine; _pid = pid; _increment = increment; } /// /// Initializes a new instance of the ObjectId class. /// /// The value. public DesignerObjectId(string value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } Unpack(ParseHexString(value) , out _timestamp , out _machine , out _pid , out _increment); } // public static properties /// /// Gets an instance of ObjectId where the value is empty. /// public static DesignerObjectId Empty => EmptyInstance; // public properties /// /// Gets the timestamp. /// public int Timestamp => _timestamp; /// /// Gets the machine. /// public int Machine => _machine; /// /// Gets the PID. /// public short Pid => _pid; /// /// Gets the increment. /// public int Increment => _increment; /// /// Gets the creation time (derived from the timestamp). /// public DateTime CreationTime => UnixEpoch.AddSeconds(_timestamp); // public operators /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId /// True if the first ObjectId is less than the second ObjectId. public static bool operator <(DesignerObjectId lhs , DesignerObjectId rhs) { return lhs.CompareTo(rhs) < 0; } /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId /// True if the first ObjectId is less than or equal to the second ObjectId. public static bool operator <=(DesignerObjectId lhs , DesignerObjectId rhs) { return lhs.CompareTo(rhs) <= 0; } /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId. /// True if the two ObjectIds are equal. public static bool operator ==(DesignerObjectId lhs , DesignerObjectId rhs) { return lhs.Equals(rhs); } /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId. /// True if the two ObjectIds are not equal. public static bool operator !=(DesignerObjectId lhs , DesignerObjectId rhs) { return !(lhs == rhs); } /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId /// True if the first ObjectId is greather than or equal to the second ObjectId. public static bool operator >=(DesignerObjectId lhs , DesignerObjectId rhs) { return lhs.CompareTo(rhs) >= 0; } /// /// Compares two ObjectIds. /// /// The first ObjectId. /// The other ObjectId /// True if the first ObjectId is greather than the second ObjectId. public static bool operator >(DesignerObjectId lhs , DesignerObjectId rhs) { return lhs.CompareTo(rhs) > 0; } // public static methods /// /// Generates a new ObjectId with a unique value. /// /// An ObjectId. public static DesignerObjectId GenerateNewId() { return GenerateNewId(GetTimestampFromDateTime(DateTime.UtcNow)); } /// /// Generates a new ObjectId with a unique value (with the timestamp component based on a given DateTime). /// /// The timestamp component (expressed as a DateTime). /// An ObjectId. public static DesignerObjectId GenerateNewId(DateTime timestamp) { return GenerateNewId(GetTimestampFromDateTime(timestamp)); } /// /// Generates a new ObjectId with a unique value (with the given timestamp). /// /// The timestamp component. /// An ObjectId. public static DesignerObjectId GenerateNewId(int timestamp) { int increment = Interlocked.Increment(ref _staticIncrement) & 0x00ffffff; // only use low order 3 bytes return new DesignerObjectId(timestamp , StaticMachine , StaticPid , increment); } /// /// Generates a new ObjectId string with a unique value. /// /// The string value of the new generated ObjectId. public static string GenerateNewStringId() { return GenerateNewId().ToString(); } /// /// Packs the components of an ObjectId into a byte array. /// /// The timestamp. /// The machine hash. /// The PID. /// The increment. /// A byte array. public static byte[] Pack(int timestamp , int machine , short pid , int increment) { if ((machine & 0xff000000) != 0) { throw new ArgumentOutOfRangeException(nameof(machine) , "The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); } if ((increment & 0xff000000) != 0) { throw new ArgumentOutOfRangeException(nameof(increment) , "The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); } byte[] bytes = new byte[12]; bytes[0] = (byte)(timestamp >> 24); bytes[1] = (byte)(timestamp >> 16); bytes[2] = (byte)(timestamp >> 8); bytes[3] = (byte)(timestamp); bytes[4] = (byte)(machine >> 16); bytes[5] = (byte)(machine >> 8); bytes[6] = (byte)(machine); bytes[7] = (byte)(pid >> 8); bytes[8] = (byte)(pid); bytes[9] = (byte)(increment >> 16); bytes[10] = (byte)(increment >> 8); bytes[11] = (byte)(increment); return bytes; } /// /// Parses a string and creates a new ObjectId. /// /// The string value. /// A ObjectId. public static DesignerObjectId Parse(string s) { if (s == null) { throw new ArgumentNullException(nameof(s)); } if (s.Length != 24) { throw new ArgumentOutOfRangeException(nameof(s) , "ObjectId string value must be 24 characters."); } return new DesignerObjectId(ParseHexString(s)); } /// /// Unpacks a byte array into the components of an ObjectId. /// /// A byte array. /// The timestamp. /// The machine hash. /// The PID. /// The increment. public static void Unpack(byte[] bytes , out int timestamp , out int machine , out short pid , out int increment) { if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } if (bytes.Length != 12) { throw new ArgumentOutOfRangeException(nameof(bytes) , "Byte array must be 12 bytes long."); } timestamp = (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3]; machine = (bytes[4] << 16) + (bytes[5] << 8) + bytes[6]; pid = (short)((bytes[7] << 8) + bytes[8]); increment = (bytes[9] << 16) + (bytes[10] << 8) + bytes[11]; } // private static methods /// /// Gets the current process id. This method exists because of how CAS operates on the call stack, checking /// for permissions before executing the method. Hence, if we inlined this call, the calling method would not execute /// before throwing an exception requiring the try/catch at an even higher level that we don't necessarily control. /// [MethodImpl(MethodImplOptions.NoInlining)] private static int GetCurrentProcessId() { return Process.GetCurrentProcess().Id; } private static int GetMachineHash() { var hostName = Environment.MachineName; // use instead of Dns.HostName so it will work offline var md5 = MD5.Create(); var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(hostName)); return (hash[0] << 16) + (hash[1] << 8) + hash[2]; // use first 3 bytes of hash } private static int GetTimestampFromDateTime(DateTime timestamp) { return (int)Math.Floor((ToUniversalTime(timestamp) - UnixEpoch).TotalSeconds); } // public methods /// /// Compares this ObjectId to another ObjectId. /// /// The other ObjectId. /// A 32-bit signed integer that indicates whether this ObjectId is less than, equal to, or greather than the other. public int CompareTo(DesignerObjectId other) { int r = _timestamp.CompareTo(other._timestamp); if (r != 0) { return r; } r = _machine.CompareTo(other._machine); if (r != 0) { return r; } r = _pid.CompareTo(other._pid); if (r != 0) { return r; } return _increment.CompareTo(other._increment); } /// /// Compares this ObjectId to another ObjectId. /// /// The other ObjectId. /// True if the two ObjectIds are equal. public bool Equals(DesignerObjectId rhs) { return _timestamp == rhs._timestamp && _machine == rhs._machine && _pid == rhs._pid && _increment == rhs._increment; } /// /// Compares this ObjectId to another object. /// /// The other object. /// True if the other object is an ObjectId and equal to this one. public override bool Equals(object obj) { if (obj is DesignerObjectId) { return Equals((DesignerObjectId)obj); } return false; } /// /// Gets the hash code. /// /// The hash code. public override int GetHashCode() { int hash = 17; hash = 37 * hash + _timestamp.GetHashCode(); hash = 37 * hash + _machine.GetHashCode(); hash = 37 * hash + _pid.GetHashCode(); hash = 37 * hash + _increment.GetHashCode(); return hash; } /// /// Converts the ObjectId to a byte array. /// /// A byte array. public byte[] ToByteArray() { return Pack(_timestamp , _machine , _pid , _increment); } /// /// Returns a string representation of the value. /// /// A string representation of the value. public override string ToString() { return ToHexString(ToByteArray()); } /// /// Parses a hex string into its equivalent byte array. /// /// The hex string to parse. /// The byte equivalent of the hex string. public static byte[] ParseHexString(string s) { if (s == null) { throw new ArgumentNullException(nameof(s)); } if (s.Length % 2 == 1) { throw new Exception("The binary key cannot have an odd number of digits"); } byte[] arr = new byte[s.Length >> 1]; for (int i = 0; i < s.Length >> 1; ++i) { arr[i] = (byte)((GetHexVal(s[i << 1]) << 4) + (GetHexVal(s[(i << 1) + 1]))); } return arr; } /// /// Converts a byte array to a hex string. /// /// The byte array. /// A hex string. public static string ToHexString(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } var result = new char[bytes.Length * 2]; for (int i = 0; i < bytes.Length; i++) { var val = Lookup32[bytes[i]]; result[2 * i] = (char)val; result[2 * i + 1] = (char)(val >> 16); } return new string(result); } /// /// Converts a DateTime to number of milliseconds since Unix epoch. /// /// A DateTime. /// Number of seconds since Unix epoch. public static long ToMillisecondsSinceEpoch(DateTime dateTime) { var utcDateTime = ToUniversalTime(dateTime); return (utcDateTime - UnixEpoch).Ticks / 10000; } /// /// Converts a DateTime to UTC (with special handling for MinValue and MaxValue). /// /// A DateTime. /// The DateTime in UTC. public static DateTime ToUniversalTime(DateTime dateTime) { if (dateTime == DateTime.MinValue) { return DateTime.SpecifyKind(DateTime.MinValue , DateTimeKind.Utc); } else if (dateTime == DateTime.MaxValue) { return DateTime.SpecifyKind(DateTime.MaxValue , DateTimeKind.Utc); } else { return dateTime.ToUniversalTime(); } } private static int GetHexVal(char hex) { int val = hex; //For uppercase A-F letters: //return val - (val < 58 ? 48 : 55); //For lowercase a-f letters: //return val - (val < 58 ? 48 : 87); //Or the two combined, but a bit slower: return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); } } }