解剖SQLSERVER 第四篇 OrcaMDF里对dates类型数据的解析(译)
http://improve.dk/parsing-dates-in-orcamdf/
在SQLSERVER里面有几种不同的date相关类型,当前OrcaMDF 支持三种最常用的date类型:date,datetime,smalldatetime
SqlDate实现
date 类型在三种类型之中是最简单的,他是一个3个字节的定长类型,存储了日期值它支持的日期范围从0001-01-01到9999-12-31
默认值是1900-01-01
比较坑爹的是.NET里面还没有任何标准实现能够支持3个字节的整数类型,只有short类型和int类型,但是,他们要不太大要不太小
另外,要正确读取日期值,对于.NET的4字节整型我们必须执行一些转变去获取正确的数字
一旦我们获取到date的值,我们可以创建一个默认的datetime类型并且添加天数进去
public class SqlDate : ISqlType { public bool IsVariableLength { get { return false ; } } public short ? FixedLength { get { return 3 ; } } public object GetValue( byte [] value) { if (value.Length != 3 ) throw new ArgumentException( " Invalid value length: " + value.Length); // Magic needed to read a 3 byte integer into .NET's 4 byte representation. // Reading backwards due to assumed little endianness. int date = (value[ 2 ] << 16 ) + (value[ 1 ] << 8 ) + value[ 0 ]; return new DateTime( 1 , 1 , 1 ).AddDays(date); } }
相关测试
using System; using NUnit.Framework; using OrcaMDF.Core.Engine.SqlTypes; namespace OrcaMDF.Core.Tests.Engine.SqlTypes { [TestFixture] public class SqlDateTests { [Test] public void GetValue() { var type = new SqlDate(); var input = new byte [] { 0xf6 , 0x4c , 0x0b }; Assert.AreEqual( new DateTime( 2028 , 09 , 09 ), Convert.ToDateTime(type.GetValue(input))); input = new byte [] { 0x71 , 0x5c , 0x0b }; Assert.AreEqual( new DateTime( 2039 , 07 , 17 ), Convert.ToDateTime(type.GetValue(input))); } [Test] public void Length() { var type = new SqlDate(); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 2 ])); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 4 ])); } } }
SqlDateTime实现
date类型只能存储日期,而datetime类型不但能存储date也能存储time
datetime存储8字节定长数据值,第一部分是time(4字节),而第二部分是date(4字节)
计算date部分跟上面介绍date类型基本上一样,不过这一次date部分是一个四字节整数,比上面的例子容易处理多了,上面的date类型是3个字节
time部分存储为自午夜时的ticks数,一个tick就是1/300th 秒,为了显示tick值,我们首先定义一个常量,常量值是10d/3d
time的各个部分实际同样存储在同一个整型值里面(比如时间,分钟,秒,毫秒),所以我们要独立访问这些单独的部分,我们必须
要执行一些转换 (包括取模和相除)
部分 计算 小时 X / 300 / 60 / 60 分钟 X / 300 / 60 % 60 秒 X / 300 % 60 毫秒 X % 300 * 10d / 3d
public class SqlDateTime : ISqlType { private const double CLOCK_TICK_MS = 10d/ 3d; public bool IsVariableLength { get { return false ; } } public short ? FixedLength { get { return 8 ; } } public object GetValue( byte [] value) { if (value.Length != 8 ) throw new ArgumentException( " Invalid value length: " + value.Length); int time = BitConverter.ToInt32(value, 0 ); int date = BitConverter.ToInt32(value, 4 ); return new DateTime( 1900 , 1 , 1 , time/ 300 / 60 / 60 , time/ 300 / 60 % 60 , time/ 300 % 60 , ( int )Math.Round(time% 300 * CLOCK_TICK_MS)).AddDays(date); } }
相关测试
using System; using NUnit.Framework; using OrcaMDF.Core.Engine.SqlTypes; namespace OrcaMDF.Core.Tests.Engine.SqlTypes { [TestFixture] public class SqlDateTimeTests { [Test] public void GetValue() { var type = new SqlDateTime(); byte [] input; input = new byte [] { 0x5e , 0x3b , 0x5d , 0x00 , 0x25 , 0x91 , 0x00 , 0x00 }; Assert.AreEqual( new DateTime( 2001 , 09 , 25 , 05 , 39 , 26 , 820 ), (DateTime)type.GetValue(input)); input = new byte [] { 0xb6 , 0x87 , 0xf0 , 0x00 , 0xd1 , 0x8b , 0x00 , 0x00 }; Assert.AreEqual( new DateTime( 1997 , 12 , 31 , 14 , 35 , 44 , 607 ), (DateTime)type.GetValue(input)); input = new byte [] { 0x2d , 0xfd , 0x1c , 0x01 , 0x4a , 0x75 , 0x00 , 0x00 }; Assert.AreEqual( new DateTime( 1982 , 03 , 18 , 17 , 17 , 36 , 790 ), (DateTime)type.GetValue(input)); input = new byte [] { 0xff , 0x81 , 0x8b , 0x01 , 0x7f , 0x24 , 0x2d , 0x00 }; Assert.AreEqual( new DateTime( 9999 , 12 , 31 , 23 , 59 , 59 , 997 ), (DateTime)type.GetValue(input)); } [Test] public void Length() { var type = new SqlDateTime(); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 9 ])); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 7 ])); } } }
SqlSmallDateTime实现
Smalldatetime 是一个不错的数据类型当你需要存储范围值内的日期值(1900~2079)并且他能精确到秒
大多数场景下,精确到秒已经足够了,在一个范围的时间间隔内和精确值不需要太精确的情况下会节省很多空间
smalldatetime 数据类型会只占用4个字节,前2个字节存储自午夜的分钟数,后2个字节存储日期,默认值是1900-1-1
处理的方法跟datetime差不多,只不过使用更小的范围
部分 计算 小时 X / 60 分钟 X % 60
public class SqlSmallDateTime : ISqlType { public bool IsVariableLength { get { return false ; } } public short ? FixedLength { get { return 4 ; } } public object GetValue( byte [] value) { if (value.Length != 4 ) throw new ArgumentException( " Invalid value length: " + value.Length); ushort time = BitConverter.ToUInt16(value, 0 ); ushort date = BitConverter.ToUInt16(value, 2 ); return new DateTime( 1900 , 1 , 1 , time / 60 , time % 60 , 0 ).AddDays(date); } }
相关测试
using System; using NUnit.Framework; using OrcaMDF.Core.Engine.SqlTypes; namespace OrcaMDF.Core.Tests.Engine.SqlTypes { [TestFixture] public class SqlSmallDateTimeTests { [Test] public void GetValue() { var type = new SqlSmallDateTime(); var input = new byte [] { 0xab , 0x02 , 0x5d , 0x26 }; Assert.AreEqual( new DateTime( 1926 , 11 , 22 , 11 , 23 , 0 ), Convert.ToDateTime(type.GetValue(input))); input = new byte [] { 0x49 , 0x03 , 0x99 , 0x09 }; Assert.AreEqual( new DateTime( 1906 , 9 , 24 , 14 , 1 , 0 ), Convert.ToDateTime(type.GetValue(input))); } [Test] public void Length() { var type = new SqlSmallDateTime(); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 3 ])); Assert.Throws <ArgumentException>(() => type.GetValue( new byte [ 5 ])); } } }
第四篇完