decimal , double , float 輸出 json 的格式問題

之前筆記 decimal 屬性輸出 JSON 時指定的格式問題 提到在專案中因為系統介接需要統一 decimal 小數位數,過程中也才發現 json.net 在輸出沒有小數的 decimal 時行為不太一樣(會補上 .0:小數點及小數點一位),最後雖然有解決問題,但解決方式自己卻不甚滿意,加上想要順帶測試 double 及 float 的行為,所以又花了一些時間找其他方法,就來看看過程遇到的問題及最後的解決方式吧

前提設定

  1. 自訂 model (包含 decimal , double , float 屬性)

    public class TestData
    {
        public decimal Salary { get; set; }
        public double ExRate { get; set; }
        public float TaxRate { get; set; }
    }
    
  2. 使用 jsonconvert

    public class RoundingJsonConverter : JsonConverter
    {
        //指定精準度
        int _precision;
        //指定四捨五入的傾向
        MidpointRounding _rounding;
            
        //預設精準度小數點下 4 位
        public RoundingJsonConverter() : this(4)
        {
        }
            
        public RoundingJsonConverter(int precision)
        : this(precision, MidpointRounding.AwayFromZero)
        {
        }
            
        public RoundingJsonConverter(int precision, MidpointRounding rounding)
        {
        _precision = precision;
        _rounding = rounding;
        }
            
        public override bool CanRead
        {
        get { return false; }
        }
            
        public override bool CanConvert(Type objectType)
        {
        return objectType == typeof(decimal);
        }
            
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
        throw new NotImplementedException();
        }
            
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
        decimal _value = (decimal)value;
        //為值補 0
        writer.WriteValue(decimal.Parse(Math.Round(_value, _precision, _rounding).ToString("0.".PadRight(2 + _precision, '0'))));
        }
    }
    

遇到問題及解決方式

  1. Math.Round 處理 float 會造成數值不正確

    Math.Round 沒有 float 版本,使用 Math.Round 會隱含將 float 轉型為 double,而造成數值不正確

    • 原始作法

      var value= 0.225f;
      Math.Round(value,2,MidpointRounding.AwayFromZero).Dump();
      
      • 數值不正確

        3floatissue

      • Math.Round 傳入 float 會隱含轉型 double

        4convertdouble

        5float

        6double

    • 新做法:先轉型為 decimal

      Math.Round(Convert.ToDecimal(value),2,MidpointRounding.AwayFromZero).Dump();
      

      7floatresult

  2. 為了指定輸出格式頻繁轉型 (ToString 再 parse 回 decimal)

    WriteJson 時移除 decimal.parse 與 Math.Round 並使用 WriteRawValue 方法

    • 原始做法

      writer.WriteValue(Math.Round(_value, _precision, _rounding).ToString("0.".PadRight(2 + _precision, '0')));
      

      1tostring

    • 新做法

      writer.WriteRawValue((Convert.ToDecimal(value)).ToString("0.".PadRight(2 + _precision, '0')));
      

      2rawvalue

    • 完整 jsonconvert 程式碼

      public class RoundingJsonConverter : JsonConverter
      {
          //指定精準度
          int _precision;
          //指定四捨五入的傾向
          MidpointRounding _rounding;
              
          //預設精準度小數點下 4 位
          public RoundingJsonConverter() : this(4)
          {
          }
              
          public RoundingJsonConverter(int precision)
              : this(precision, MidpointRounding.AwayFromZero)
          {
          }
              
          public RoundingJsonConverter(int precision, MidpointRounding rounding)
          {
              _precision = precision;
              _rounding = rounding;
          }
              
          public override bool CanRead
          {
              get { return false; }
          }
              
          public override bool CanConvert(Type objectType)
          {
              return objectType == typeof(decimal);
          }
              
          public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
          {
              throw new NotImplementedException();
          }
              
          public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
          {
              decimal _value = (decimal)value;
              //為值補 0
              writer.WriteRawValue((Convert.ToDecimal(value)).ToString("0.".PadRight(2 + _precision, '0')));
          }
      }
      
  3. 整數不輸出小數點及小數位

    加上屬性來控制是否將數字小數位末端的 0 移除

    • 原做法

      8notruncate

    • 新做法

      • 加入屬性用來判斷是否要移除

        //用來判斷是否移除最後的 0
        bool _truncate;
                    
        public RoundingJsonConverter(int precision, bool truncate) : this(precision, MidpointRounding.AwayFromZero, truncate)
        {
        }
                    
        public RoundingJsonConverter(int precision) : this(precision, MidpointRounding.AwayFromZero, false)
        {
        }
        
        public RoundingJsonConverter(int precision, MidpointRounding rounding, bool truncate)
        {
            _precision = precision;
            _rounding = rounding;
            _truncate = truncate;
        }
        
      • 加入 0 的處理流程

        if (_truncate)
        {
            var _result = Math.Round(_value, _precision, _rounding) / 1.000000000000000000000000000000000m;//移除0
            if (Int64.TryParse(_result.ToString(), out var longresult))//處理整數
                writer.WriteValue(longresult);
            else//處理非整數
                writer.WriteValue(_result);
        }
        
      • 在 model 上加入是否移除 0

        public class TestData
        {
            [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
            public decimal Salary { get; set; }
            [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
            public double ExRate { get; set; }
            [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
            public float TaxRate { get; set; }
        }
        

        9truncateresult

    • 完整程式碼

      void Main()
      {
          var test = new TestData() { Salary = 1.995m, ExRate = 2.295D, TaxRate = 1f };
          test.Dump();
          JsonConvert.SerializeObject(test).Dump();
      }
      public class TestData
      {
          [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
          public decimal Salary { get; set; }
          [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
          public double ExRate { get; set; }
          [JsonConverter(typeof(RoundingJsonConverter), 2, true)]
          public float TaxRate { get; set; }
      }
              
      public class RoundingJsonConverter : JsonConverter
      {
          //指定精準度
          int _precision;
          //指定四捨五入的傾向
          MidpointRounding _rounding;
          //用來判斷是否移除最後的 0
          bool _truncate;
              
          //預設精準度小數點下 4 位
          public RoundingJsonConverter() : this(4)
          {
          }
          public RoundingJsonConverter(int precision, bool truncate)
                  : this(precision, MidpointRounding.AwayFromZero, truncate)
          {
          }
          public RoundingJsonConverter(int precision)
              : this(precision, MidpointRounding.AwayFromZero, false)
          {
          }
              
          public RoundingJsonConverter(int precision, MidpointRounding rounding, bool truncate)
          {
              _precision = precision;
              _rounding = rounding;
              _truncate = truncate;
          }
              
          public override bool CanRead
          {
              get { return false; }
          }
              
          public override bool CanConvert(Type objectType)
          {
              return objectType == typeof(decimal) | objectType == typeof(double) | objectType == typeof(float);
              //return objectType == typeof(decimal);
          }
              
          public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
          {
              throw new NotImplementedException();
          }
              
          public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
          {
              var _value = Convert.ToDecimal(value);
              if (_truncate)
              {
                  var _result = Math.Round(_value, _precision, _rounding) / 1.000000000000000000000000000000000m;//移除0
                  if (Int64.TryParse(_result.ToString(), out var longresult))//處理整數
                      writer.WriteValue(longresult);
                  else//處理非整數
                      writer.WriteValue(_result);
              }
              else
                  writer.WriteRawValue(_value.ToString("0.".PadRight(2 + _precision, '0')));
          }
      }
      

心得

一開始從想要統一 decimal 的輸出格式,到後來持續調整寫法,到額外擴充支援 double 與 float,最終也可以自訂是否輸出可以刪除的小數點及 0,也許再次用到的機會並不高,但為了 coding for fun 帶來的樂趣無價呀

參考資訊

  1. decimal 屬性輸出 JSON 時指定的格式問題
  2. 關於 Decimal 小數尾數零