美文网首页Ruby & RailsRuby
Ruby里Time#to_json的两种格式

Ruby里Time#to_json的两种格式

作者: 李大白很黑 | 来源:发表于2017-08-26 02:12 被阅读38次

    晚上在接入方的提醒下,注意到了一个之前没在意的问题:OPEN API中所有返回的json时间格式都是2017-08-25T21:30:28.388Z这样的。

    “这个不好解析”,接入方如是说。

    “大概是没法一行解析吧”,同事这么讲。

    快醒醒,他们用的可是java,java怎么可能只写一行,分分钟给你写个大项目出来!

    public static Date stringToDate(String dateStr) {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
            Date date = null;
            try {
                date = format.parse(dateStr);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(Constants.LOG_TAG_C, "String to date error : " + e, e);
            }
            return date;
        }
    
    

    好吧,回归正题,我这才意识到为什么这个时间的格式是这样子的。

    我尝试在repl查看结果:

    2.4.1 :011 > PlayRecord.last.created_at.to_json
     => "\"2017-08-25T21:30:28.388Z\""
    2.4.1 :012 > PlayRecord.last.created_at.as_json
     => "2017-08-25T21:30:28.388Z"
    2.4.1 :013 > PlayRecord.last.created_at.to_s
     => "2017-08-25 21:30:28 UTC"
    2.4.1 :014 > PlayRecord.last.created_at.class
     => Time
    2.4.1 :015 > Time.now.to_s
     => "2017-08-25 21:39:50 +0800"
    2.4.1 :016 > Time.now.to_json
     => "\"2017-08-25T21:39:56.335+08:00\""
    

    Time对象调用to_json函数得到得到的结果不一样,调用to_s的结果也不一样,to_json底层甚至没有按照我设想的调用to_s方法。

    第一个反应ActiveRecord以及ActiveSupport给Time类增加了内容,就在github的茫茫多源码里找,未果。

    我要怎么知道具体的to_json函数究竟写了点什么呢?

    Google “how to get source code ruby”

    https://stackoverflow.com/questions/3393096/how-can-i-get-source-code-of-a-method-dynamically-and-also-which-file-is-this-me

    通过 method方法拿到具体某个方法对象,调用source_location可以拿到原文件的位置。

    想再偷懒一点还可以直接打印到屏幕上

    require 'method_source'
    Set.instance_method(:merge).source.display
    

    有了这两个内省的方法,就可以很方便的找到最终的位置了

    /active_support/core_ext/object/json.rb 这个文件给Time类加了as_json的函数

    (实际上,grape api 在最后返回的时候并不是简单的调用JSON.dump(data),而是调用了to_json来拿到最后的json字符串,但是to_json实际调用的是ActiveSupport::JSON.encode(self, options),最后一路找下去实际上就是
    stringify jsonify value.as_json(options.dup),本质还是在调用as_json):

    class Time
      def as_json(options = nil) #:nodoc:
        if ActiveSupport::JSON::Encoding.use_standard_json_time_format
          xmlschema(ActiveSupport::JSON::Encoding.time_precision)
        else
          %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
        end
      end
    end
    

    具体在Time的xmlschema函数里会根据是否是utc时间来给出不同的格式,这里会直接format,并不会调用to_s

    def xmlschema(fraction_digits=0)
        fraction_digits = fraction_digits.to_i
        s = strftime("%FT%T")
        if fraction_digits > 0
          s << strftime(".%#{fraction_digits}N")
        end
        s << (utc? ? 'Z' : strftime("%:z"))
      end
    

    到了这里就发现,分叉的地方其实是utc?这个函数,当我们继续使用source_location的时候得到了一个nil,但是调用却是没问题的。

    2.4.1 :003 > PlayRecord.last.created_at.utc?
     => true
    2.4.1 :004 > Time.now.utc?
     => false
    

    答案终于揭晓了,从ActiveRecord从数据库读取到的时间戳是UTC的,而本地Time.now得到的时间是有时区的。s << (utc? ? 'Z' : strftime("%:z"))这一样代码决定了最后出来的格式。

    自然顺着这个思路,我们去看to_s这个方法,同样也是找不到源代码,找不到源代码的原因是因为是由ruby解释器实现的,在ruby-doc.org上可以看到具体的源代码,同样根据是否是UTC时间做了判断:

    static VALUE
    time_to_s(VALUE time)
    {
        struct time_object *tobj;
    
        GetTimeval(time, tobj);
        if (TIME_UTC_P(tobj))
            return strftimev("%Y-%m-%d %H:%M:%S UTC", time, rb_usascii_encoding());
        else
            return strftimev("%Y-%m-%d %H:%M:%S %z", time, rb_usascii_encoding());
    }
    

    那么问题来了,既然是本地,在jruby下会怎样呢。
    我特意去找了jruby的源码(/core/src/main/java/org/jruby/RubyTime.java):

        private final static DateTimeFormatter TO_S_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss Z").withLocale(Locale.ENGLISH);
        private final static DateTimeFormatter TO_S_UTC_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss 'UTC'").withLocale(Locale.ENGLISH);
    
        @Override
        @JRubyMethod(name = {"to_s", "inspect"})
        public IRubyObject to_s() {
            return inspectCommon(TO_S_FORMATTER, TO_S_UTC_FORMATTER);
        }
        private IRubyObject inspectCommon(DateTimeFormatter formatter, DateTimeFormatter utcFormatter) {
            DateTimeFormatter simpleDateFormat;
            if (dt.getZone() == DateTimeZone.UTC) {
                simpleDateFormat = utcFormatter;
            } else {
                simpleDateFormat = formatter;
            }
    
            String result = simpleDateFormat.print(dt);
    
            if (isTzRelative) {
                // display format needs to invert the UTC offset if this object was
                // created with a specific offset in the 7-arg form of #new
                DateTimeZone dtz = dt.getZone();
                int offset = dtz.toTimeZone().getOffset(dt.getMillis());
                DateTimeZone invertedDTZ = DateTimeZone.forOffsetMillis(offset);
                DateTime invertedDT = dt.withZone(invertedDTZ);
                result = simpleDateFormat.print(invertedDT);
            }
    
            return RubyString.newString(getRuntime(), result, USASCIIEncoding.INSTANCE);
        }
    
    

    (嗯,11行的C代码对应30行Java,Java除了啰嗦以外真的没黑点)

    一样的逻辑,根据是否为utc时间来决定最后显示UTC还是+08:00。这个应该是写入语言规范了。

    所以,最后,搞了这么多,有什么用呢?

    没什么用,因为这个问题并不需要解决,形如2017-08-25T21:30:28.388Z实际上是iso8601的标准,对于utc时间后面加大写字母Z,非utc时间加时区偏移量。

    不过既然到了最后,还是传授点经验,辣鸡的移动版safari的Date.parse并不能解析这个时间,甚至它都不能解析这样的2017-08-25 21:30:28的时间,因为它要求日期的分割符是/,当然你可以说“我已经不手写Date.parse了!”。但是不妨碍一起鄙视移动版safari,顺带再黑一次java

    function parseISO8601(iso_str){
      var s = '2011-06-21T14:27:28.593Z';
      var a = s.split(/[^0-9]/);
      return new Date (a[0],a[1]-1,a[2],a[3],a[4],a[5] );
    }
    

    唉,我对Java真是爱的深沉。

    相关文章

      网友评论

      本文标题:Ruby里Time#to_json的两种格式

      本文链接:https://www.haomeiwen.com/subject/lhtmdxtx.html