2014/4/9

在 JSON.NET 中自動對應 JavaScript 時間

JSON.NET 可以讓我們很方便地讀寫 JSON 字串並對應到 C# 類別。在大部份的時候我們都可以直接使用 SerializeObject() 和 DeserializeObject() 方法進行轉換。但是一旦遇到 JavaScript 日期, 這招可能就行不通了。因為有些 JavaScript 開發人員會使用一種特殊的 JavaScript 日期表示法 (不是 Date 型別, 而是 Int64 型別) 來代表日期

若採用這種做法, 我們可能會在某個 JSON 檔案中看到以下這種資料格式:

{ "dt":1389843900000 }

這是什麼格式? 事實上, 如果你以後看到這種資料格式, 你應該就能判斷這就是一個日期, 但是其內容是以一個 Int64 型別的資料來表示。它實際上是從 1970/1/1 0:00:00 起算的毫秒數 (UTC 時間)。在 JavaScript 中, 我們可以使用 Date.UTC() 方法以取得這個值, 例如 Date.UTC(1970, 0, 1) 會得到 0。請注意這個 JavaScript 中最奇特的地方: 參數中月份是從 0 起算, 所以 0 是代表一月; 如果你輸入 12, 它會自動進位為隔年的一月。日期則是從 1 起算。同樣的, 如果你在日期欄位輸入 0, 它會視為前一天。這是 JavaScript 語言中最著名的陷阱之一, 要特別留意。若執行 Date.UTC(2014,3,9,23,59,59) (表示 2014/4/9 23:59:59) 則會得到 1397087999000。這裡的時間都是 UTC 時間, 所以我們使用時要再轉換到台灣時間 (UTC + 8 小時)。

我查了很久, 似乎這種表示式並沒有什麼特殊的名稱, 就是 "millisends" 而已。除了這種格式, 我們也常常見到另一種格式, 是以秒為單位的。所以如果你看到的日期只有十位數字, 那麼把它乘上 1000, 就等於本文中要轉換的時間單位了。

如果要在 JavaScript 中把這個數字轉回日期, 使用 new Date(1397087999000) 就可以傳回一個 Date 格式的物件, 而且它會自動幫你轉換成當地時間 (在此例中為 2014/4/10 7:59:59)。請注意一定要加上 new 關鍵字, 因為只有建構子會幫你做這個轉換。如果你忘了加上 new 字, 不管是 Date(1397087999000) 或者 Date(0) 都會默默地傳回現在時間, 不會出現任何錯誤訊息。

因此, 範例中的1389843900000 實際上就代表 2014/1/16 上午 11:45:00 (台灣時間)。知道其邏輯之後, 我們就可以在 C# 中簡單地寫個程式進行轉換。

但是, 我們要如何在 C# 類別中與 JSON 資料進行這種轉換呢? 如果你把這個欄位在 C# 類別中標示為 DateTime, 那麼一進行轉換, 就會出問題, 因為型別不對。我們有什麼辦法讓它自動進行轉換嗎?

例如,  假設你有一個 C# 類別如下:

// 程式一
[JsonObject(MemberSerialization.OptIn)]
public class EventTime
{
    [JsonProperty("id")]
    public int EventTimeId { get; set; }

    [JsonProperty("time")]
    public DateTime? Time { get; set; }
}

若照這種寫法, 程式根本無法執行。

我們必須自訂 Converter, 來做自動轉換的動作。為讀者方便起見, 我把我寫的轉換程式列在下面:

// 程式二
public class JsTimeConverter : JsonConverter
{
 public override bool CanConvert(Type objectType)
 {
     return objectType == typeof(DateTime);
 }

 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
 {
     if (reader.TokenType == JsonToken.None) return null;
     var time = (long)serializer.Deserialize(reader, typeof(long));
     return ConvertJsTimeToNormalTime(time, true);
 }

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
 {
     var item = (DateTime)value;
     writer.WriteValue(ConvertToJsTime(item));
     writer.Flush();
 }

 public long ConvertToJsTime(DateTime value)
 {
     return (long)value.ToUniversalTime()
             .Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
             .TotalMilliseconds;
 }

 public static DateTime ConvertJsTimeToNormalTime(long ticks, bool isTaiwanTime = true)
 {
     return new DateTime(1970, 1, 1).AddMilliseconds(ticks).AddHours(isTaiwanTime ? 8 : 0);
 }
}

我們必須手動撰寫一個繼承 JsonConverter 的類別以進行轉換。其中的 WriteJson() 方法是輸出為 JSON 格式, ReadJson() 則是把 JSON 格式轉換為 C# 物件。把程式二加到你的程式裡, 然後再把程式一中的類別改成如下:

// 程式三
[JsonObject(MemberSerialization.OptIn)]
public class EventTime
{
    [JsonProperty("id")]
    public int EventTimeId { get; set; }

    [JsonProperty("time")]
    [JsonConverter(typeof(JsTimeConverter))]
    public DateTime? Time { get; set; }
}

我們藉由加入 JsonConverter 這個 attribute, 指定針對該欄位 (Time) 的轉換程式。當我們使用 SerializeObject() 和 DeserializeObject() 方法讀寫 JSON 檔案時, 這轉換程式就會自動被呼叫, 做好無縫的格式轉換。

以下我把我的測試程式列出來:

// 程式四
static void Main(string[] args)
{
    string json = "[ {\"id\": 1, \"time\":1389843900000}, {\"id\": 2, \"time\": 0} ]";
    List<EventTime> list = testConverterIn(json);
    Console.WriteLine(testConverterOut(list));
    Console.WriteLine("Press any key to exit... ");
    Console.ReadKey();
}

private static List<EventTime> testConverterIn(string input)
{
    List<EventTime> list = new List<EventTime>();
    List<EventTime> withTimes = null;            
    EventTimes = JsonConvert.DeserializeObject<List<EventTime>>(input);
    foreach (EventTime wt in EventTimes)
    {
        Console.WriteLine("{0}. {1}", wt.EventTimeId, wt.Time);
        list.Add(wt);
    }
    return list;
}

private static string testConverterOut(List<EventTime> input)
{
    string output = JsonConvert.SerializeObject(input);
    return output;
}

如果你有其它種類的轉換必要, 原則上就是如上所寫的, 先把繼承 JsonConverter 的工具類別寫好, 在裡面加上你自己的邏輯, 然後在對應的類別中, 在必須進行轉換的欄位上加上 [JsonConverter(typeof(你的轉換程式類別))] 即可。

以下是所有桯式的列表:

// 程式五
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JsonTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string json = "[ {\"id\": 1, \"time\":1389843900000}, {\"id\": 2, \"time\": 0} ]";
            List<EventTime> list = testConverterIn(json);
            Console.WriteLine(testConverterOut(list));
            Console.WriteLine("Press any key to exit... ");
            Console.ReadKey();
        }

        private static List<EventTime> testConverterIn(string input)
        {
            List<EventTime> list = new List<EventTime>();
            List<EventTime> EventTimes = null;            
            EventTimes = JsonConvert.DeserializeObject<List<EventTime>>(input);
            foreach (EventTime wt in EventTimes)
            {
                Console.WriteLine("{0}. {1}", wt.EventTimeId, wt.Time);
                list.Add(wt);
            }
            return list;
        }

        private static string testConverterOut(List<EventTime> input)
        {
            string output = JsonConvert.SerializeObject(input);
            return output;
        }
    }

    public class JsTimeConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(DateTime);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.None) return null;
            var time = (long)serializer.Deserialize(reader, typeof(long));
            return ConvertJsTimeToNormalTime(time, true);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var item = (DateTime)value;
            writer.WriteValue(ConvertToJsTime(item));
            writer.Flush();
        }

        public long ConvertToJsTime(DateTime value)
        {
            return (long)value.ToUniversalTime()
                   .Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
                   .TotalMilliseconds;
        }

        public static DateTime ConvertJsTimeToNormalTime(long ticks, bool isTaiwanTime = true)
        {
            return new DateTime(1970, 1, 1).AddMilliseconds(ticks).AddHours(isTaiwanTime ? 8 : 0);
        }
    }

    [JsonObject(MemberSerialization.OptIn)]
    public class EventTime
    {
        [JsonProperty("id")]
        public int EventTimeId { get; set; }

        [JsonProperty("time")]
        [JsonConverter(typeof(JsTimeConverter))]
        public DateTime? Time { get; set; }
    }
}

沒有留言:

張貼留言