美文网首页
代码重构之 (时间显示策略)

代码重构之 (时间显示策略)

作者: 专家搬运工 | 来源:发表于2019-06-20 21:46 被阅读0次

    背景

    我们 App 非常重视时间的显示规则,比如在首页显示的新闻如果是一个礼拜之前的,那就应该隐藏时间,避免让用户感觉该新闻非常陈旧,但是其他场景比如用户的评论,就不会在乎时效性了。

    之前新闻App针对不同的时间策略,写了不同的函数来处理显示逻辑,这些函数内容雷同冗长,又不方便复用代码,每次新增一个时间显示策略,又得copy一份代码,然后一顿修改。
    没有任何扩展性可言。

    现在我们提供了一个时间显示的工具类 XXCustomDateFormatter(前缀保密),它可以在远程动态配置时间显示策略,如果是修改某个场景的策略,客户端无需修改代码,如果是新增场景,客户端仅仅需要新增一个 type 即可。

    用法

    目前已有的时间显示策略如下:

    typedef NS_ENUM(NSInteger, CustomDateFormatterType) {
        kCustomDateFormatterTypeDefault = 0,          // 默认场景
        kCustomDateFormatterTypeTimeLine,             // 主TimeLine
        ...........
    };
    

    如果你对应的业务使用的是默认策略,那么如下一行代码即可获取你想要的string

    NSString *timeMsg = [XXCustomDateFormatter customStringWithTimeInterval:listItem.timeStamp type:kCustomDateFormatterTypeDefault];
    

    原理

    我们在远程可以定义多个字典,每个字典对应一个特殊的场景,每个场景的规则都不一样,key的前缀目前包含 T(秒) D(日) Y(年) MAX(最大值),value 对应的就是显示给用户看的时间,比如value = “yyyy年MM月dd日” 代表我们给用户呈现的效果是年月日。

    估计目前为止读者还是一脸懵逼,这件事的确不太好理解。接下来我举一个例子。

    时间策略实例:

    // 默认时间显示策略
    static NSString *kQNDefaultTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
                                                      \"T-3600\": \"%m分钟前\",\
                                                      \"D-0\": \"%h小时前\",\
                                                      \"D-1\": \"昨天HH:mm\",\
                                                      \"D-2\": \"前天HH:mm\",\
                                                      \"Y-0\": \"MM月dd日HH:mm\",\
                                                      \"MAX-0\": \"yyyy年MM月dd日\",\
                                                      \"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|Y-0|MAX-0\"}";
    

    这是我们配置的一个字典,该字典用于默认场景,我们客户端如何解析这个字典呢?
    现在我们假设有一个服务器下发的时间戳叫 listItem. timeStamp,它代表一篇文章的发布时间,listItem. timeStamp 与当前的时间差叫做 time.

    首先,我们根据 keyOrder 来决定顺序,首先来到 T-60, 我们把time和60做比较,如果小于60客户端就显示这篇文章的发布时间为 “刚刚”
    如果time < 3600,我们就显示这篇文章的发布时间为 “%m分钟前”,当然这个%m需要我们自己计算出来并替换成真实的时间。
    接下来就好理解了, D-0 代表如果文章是今日之内发布的,我就显示成 "%h小时前" ,这个 h当然也要客户端计算,同理 D-1 代表昨天发布的,Y-0代表今年发布,Y-1代表去年发布的。

    源码

    h文件

    
    #import <Foundation/Foundation.h>
    
    /**
     根据CommonValues动态地格式化时间
     更具有灵活性
     
     下发时间配置示例:
     "time_line_time_display_config": {
         "MAX-259200": "",
         "T-60": "刚刚",
         "T-3600": "%m分钟前",
         "T-28800": "%h小时前",
         "D-0": "8小时前",
         "D-1": "昨天HH:mm",
         "D-2": "前天HH:mm",
         "keyOrder": "MAX-259200|T-60|T-3600|T-28800|D-0|D-1|D-2"
     }
     
     具体规则如下见 http://tapd.oa.com/newsapp_android/markdown_wikis/view/#1010056241007851563
     */
    
    typedef NS_ENUM(NSInteger, CustomDateFormatterType) {
        kCustomDateFormatterTypeDefault = 0,          // 默认场景
        kCustomDateFormatterTypeSimple,               // 简单版本时间显示(比如用于用户评论)
    };
    
    @interface XXCustomDateFormatter : NSObject
    
    /**
     根据CommonValues下发配置动态地格式化时间
     
     @param timeInterval 时间
     @param type 时间显示场景
     @return 格式化后的时间
     */
    + (NSString *)customStringWithTimeInterval:(NSTimeInterval)timeInterval type:(CustomDateFormatterType)type;
    
    /**
     保存一份commonValues到QNCustomDateFormatter
     (因为它也用于24 Hour widget Extension这个target,不适合引用CRemoteConfig)
     
     @param dic 最新的commonValues
     */
    + (void)updateCommonValueWithDic:(NSDictionary *)dic;
    
    @end
    
    

    m文件

    
    #import "XXCustomDateFormatter.h"
    
    static NSDictionary *commonValues;
    
    // key 匹配需要的pattern
    static NSString *kQNKeyPatternMax = @"MAX-";         // 后面携带的是秒单位的时间点,大于当前时间点,则匹配不显示
    static NSString *kQNKeyPatternTime = @"T-";          // 后面携带的是秒单位的时间点,小于当前时间点则匹配
    static NSString *kQNKeyPatternDay = @"D-";           // 后面携带的是和今天差的天数,0:今天;1:昨天;以此类推
    static NSString *kQNKeyPatternYear = @"Y-";          // 后面写带的是和当年差的年数,0:当年,1:去年;以此类推
    static NSString *kQNKeyPatternAbsolute = @"ABS-";    // 绝对日期,后面匹配的是x.x.x,代表的年月日,x的情况下代表任意 ABS-x.1.1 某年第一天
    
    // value 匹配需要的pattern
    static NSString *kQNValuePatternSecond = @"%s";      // 把时间差按照秒替换
    static NSString *kQNValuePatternMinute = @"%m";      // 把时间差按照分钟替换
    static NSString *kQNValuePatternHour = @"%h";        // 把时间差按照小时替换
    static NSString *kQNValuePatternDay = @"%d";         // 把时间差按照天替换
    static NSString *kQNValuePatternWeek = @"%w";        // 把时间差按照周替换
    
    
    // 简单版本时间(比如用于用户评论)显示策略
    static NSString *kQNSimpleTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
                                                     \"T-3600\": \"%m分钟前\",\
                                                     \"D-0\": \"%h小时前\",\
                                                     \"D-1\": \"昨天\",\
                                                     \"D-2\": \"前天\",\
                                                     \"MAX-0\": \"MM月dd日\",\
                                                     \"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|MAX-0\"}";
    
    // 默认时间显示策略
    static NSString *kQNDefaultTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
                                                      \"T-3600\": \"%m分钟前\",\
                                                      \"D-0\": \"%h小时前\",\
                                                      \"D-1\": \"昨天HH:mm\",\
                                                      \"D-2\": \"前天HH:mm\",\
                                                      \"Y-0\": \"MM月dd日HH:mm\",\
                                                      \"MAX-0\": \"yyyy年MM月dd日\",\
                                                      \"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|Y-0|MAX-0\"}";
    
    @implementation XXCustomDateFormatter
    
    + (NSString *)customStringWithTimeInterval:(NSTimeInterval)timeInterval type:(CustomDateFormatterType)type {
        NSDictionary *dic = [self _getConfigDicWithType:type];
        
        // 获取失败或者defaut情况下使用默认策略:DEFAULT_TIME_DISPLAY_DEFAULT
        if (!CHECK_VALID_DICTIONARY(dic) || !CHECK_VALID_STRING(dic[@"keyOrder"])) {
            dic = [NSJSONSerialization JSONObjectWithData:[kQNDefaultTimeDisplayConfig dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
            if (!CHECK_VALID_DICTIONARY(dic) || !CHECK_VALID_STRING(dic[@"keyOrder"])) {
                QN_ASSERT(NO, @"NSJSONSerialization JSONObjectWithData Error");
                return @"";
            }
        }
        
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
        time_t pubdate = [date timeIntervalSince1970];
        struct tm *pubDate = localtime((const time_t *)&pubdate);
        
        // 时间戳异常
        if (pubDate == NULL) {
            QN_E(@"zhiyun invalid date, timeInterval = %@", @(timeInterval));
            return @"";
        }
        
        int yearOfPubDate = pubDate->tm_year + 1900;
        int monOfPubDate = pubDate->tm_mon + 1;
        int dayOfPubDate = pubDate->tm_mday;
        int hourOfPubDate = pubDate->tm_hour;
        int minOfPubDate = pubDate->tm_min;
        
        time_t now = time(0);
        struct tm *today = localtime((const time_t *)&now);
        int yearOfToday = today->tm_year + 1900;
        
        // 现在与发布时间的时间差
        NSInteger times = labs(now - pubdate);
        
        NSString *keyOrder = dic[@"keyOrder"];
        
        __block NSString *resultStringDate = @"";
        NSArray<NSString *> *keyArray = [keyOrder componentsSeparatedByString:@"|"];
        [keyArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSRange range = [obj rangeOfString:@"-"];
            NSUInteger index = range.location;
            NSString *data = [obj substringFromIndex:(index + 1)];
            NSInteger numberInKey = data.integerValue;
            
            NSString *value = dic[obj];
            
            if ([obj containsString:kQNKeyPatternMax]) { // MAX-
                if (times > numberInKey) {
                    resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:value
                                                                           minOfPubDate:minOfPubDate
                                                                          hourOfPubDate:hourOfPubDate
                                                                           dayOfPubDate:dayOfPubDate
                                                                           monOfPubDate:monOfPubDate
                                                                          yearOfPubDate:yearOfPubDate
                                                                                  times:times];
                    *stop = YES;
                }
            } else if ([obj containsString:kQNKeyPatternTime]) { // T-
                if (times < numberInKey) {
                    resultStringDate = value;
                    if ([value containsString:kQNValuePatternSecond]) { // 秒
                        resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternSecond withString:[NSString stringWithFormat:@"%zd", times]];
                    } else if ([value containsString:kQNValuePatternMinute]) { // 分钟
                        resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternMinute withString:[NSString stringWithFormat:@"%zd", (times / 60)]];
                    } else if ([value containsString:kQNValuePatternHour]) { // 小时
                        resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternHour withString:[NSString stringWithFormat:@"%zd", (times / 3600)]];
                    }
                    *stop = YES;
                }
            } else if ([obj containsString:kQNKeyPatternDay]) { // D-
                NSCalendar *calendar = [NSCalendar currentCalendar];
                // 自己构造的时间,代表发布时间当天0点
                NSDate *pubDate = [calendar dateBySettingHour:0 minute:0 second:0 ofDate:date options:0];
                NSDate *nowDate = [NSDate date];
                nowDate = [calendar dateBySettingHour:0 minute:0 second:0 ofDate:nowDate options:0];
                NSTimeInterval interval = [nowDate timeIntervalSinceDate:pubDate];
                interval = fabs(interval / 3600);
                
                if (interval < 49) { // 今天、昨天或前天 interval == 0 || interval == 24 || interval == 48
                    // 先把key拼接出来
                    NSString *customKey = [NSString stringWithFormat:@"%@%d", kQNKeyPatternDay, (int)(interval / 24)];
                    resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:dic[customKey]
                                                                           minOfPubDate:minOfPubDate
                                                                          hourOfPubDate:hourOfPubDate
                                                                           dayOfPubDate:dayOfPubDate
                                                                           monOfPubDate:monOfPubDate
                                                                          yearOfPubDate:yearOfPubDate
                                                                                  times:times];
                    // 避免特殊情况出现bug,比如只下发了D-0,没有下发D-1,而恰好时间是昨天,就无法从字典中就获取到D-1对应的value
                    if (![resultStringDate isEqualToString:@""])
                        *stop = YES;
                }
            } else if ([obj containsString:kQNKeyPatternYear]) { // Y-
                if (yearOfPubDate == yearOfToday) {
                    resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:value
                                                                           minOfPubDate:minOfPubDate
                                                                          hourOfPubDate:hourOfPubDate
                                                                           dayOfPubDate:dayOfPubDate
                                                                           monOfPubDate:monOfPubDate
                                                                          yearOfPubDate:yearOfPubDate
                                                                                  times:times];
                    *stop = YES;
                }
            }
        }];
        
        return resultStringDate;
    }
    
    /**
     把value中的时间占位符替换成真正的时间,比如把yyyy替换成 2019
    
     @param configValue 带时间占位符的原始字符串
     @param minOfPubDate minOfPubDate description
     @param hourOfPubDate hourOfPubDate description
     @param dayOfPubDate dayOfPubDate description
     @param monOfPubDate monOfPubDate description
     @param yearOfPubDate yearOfPubDate description
     @param times times description
     @return 包含真正时间的结果
     */
    + (NSString *)formatDateWithConfigValue:(NSString *)configValue minOfPubDate:(int)minOfPubDate hourOfPubDate:(int)hourOfPubDate dayOfPubDate:(int)dayOfPubDate monOfPubDate:(int)monOfPubDate yearOfPubDate:(int)yearOfPubDate times:(NSInteger)times{
        
        if (!CHECK_VALID_STRING(configValue)) {
            return @"";
        }
        
        NSMutableString *result = [NSMutableString stringWithString:configValue];
        if ([result containsString:kQNValuePatternHour]) { // 几小时前
            [result replaceOccurrencesOfString:kQNValuePatternHour withString:[NSString stringWithFormat:@"%zd", (times / 3600)] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
            return result;
        }
        [result replaceOccurrencesOfString:@"mm" withString:[NSString stringWithFormat:@"%02d", minOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
        [result replaceOccurrencesOfString:@"HH" withString:[NSString stringWithFormat:@"%02d", hourOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
        [result replaceOccurrencesOfString:@"dd" withString:[NSString stringWithFormat:@"%02d", dayOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
        [result replaceOccurrencesOfString:@"MM" withString:[NSString stringWithFormat:@"%02d", monOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
        [result replaceOccurrencesOfString:@"yyyy" withString:[NSString stringWithFormat:@"%02d", yearOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
        
        return result;
    }
    
    + (void)updateCommonValueWithDic:(NSDictionary *)dic {
        commonValues = dic;
    }
    
    /**
     根据key值从CommonValue 中获取时间配置
    
     @param key 某个显示时间的场景对应的key值
     @return 时间配置字典
     */
    + (NSDictionary *)p_dictionaryValueInCommonValuesWithKey:(NSString *)key {
        QN_ASSERT(CHECK_VALID_STRING(key), @"key is invalid.");
        NSDictionary *result = nil;
        if (CHECK_VALID_DICTIONARY(commonValues)) {
            result = QNDictionary(commonValues[key], nil);
        }
        return result;
    }
    
    
    /**
     根据场景获取时间配置字典
    
     @param type 场景
     @return 时间配置字典
     */
    + (NSDictionary *)_getConfigDicWithType:(CustomDateFormatterType)type {
        NSDictionary *dic = [NSDictionary dictionary];
        switch (type) {
            case kCustomDateFormatterTypeTimeLine:
                dic = [self p_dictionaryValueInCommonValuesWithKey:@"time_line_time_display_config"];
                break;
            case kCustomDateFormatterTypeSimple:
                dic = [NSJSONSerialization JSONObjectWithData:[kQNSimpleTimeDisplayConfig dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
                break;
    
            default: // 使用默认策略:DEFAULT_TIME_DISPLAY_DEFAULT
                break;
        }
        
        return dic;
    }
    
    @end
    
    

    相关文章

      网友评论

          本文标题:代码重构之 (时间显示策略)

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