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; } } }
沒有留言:
張貼留言