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

背景

我们 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271