NSDate 日期转换为对应的星座或生肖

最近遇到个需求,需将日期转换为与之对应的占星星座( Astrological Zodiac Sign)
及生肖(Chinese Zodiac Sign)。在讨论具体实现前,我们可以先对时间处理从概念到实现上进行一些了解和回顾。
首先,从概念上,我们知道时间是独立存在的,日期是时间在特定历法中的表现。当然,在实际生活中,日期还受地理时区等因素影响。从概念出发,我们可以理解,平时讨论的日期更多地是指特定历法和时区下的某个时间。比如北京时间9点,公历2016年9月1日,农历八月十五等。
而在Cocoa框架中,苹果对时间和日期的处理方式是这样的:

1
time -> NSTimeInterva -> NSDate -> NSCalendar/NSTimeZone/NSDateComponents/NSDateComponentFormatter/NSDateFormatter -> date

在这里,苹果使用NSTimeInterva,以时间戳的方式表示时间本身:

1
2
3
typedef double NSTimeInterval;

Used to specify a time interval, in seconds.

但是,从NSTimeInterva的声明上,我们可以知道时间戳是不具像的,直接使用时间戳进行时间比较或计算是不太方便的。因而苹果使用NSDate对时间戳进行了处理,使其对象化,便于外部调用。而在NSDate的设计上,苹果将复杂的时间戳计算隐藏于NSDate的内部实现内,同时也让NSDate保留时间戳的独立性,让其成为一种对象化的时间戳:

1
NSDate objects encapsulate a single point in time, independent of any particular calendrical system or time zone. Date objects are immutable, representing an invariant time interval relative to an absolute reference date (00:00:00 UTC on 1 January 2001).

这里,苹果希望NSDate独立于任何历法或地理时区,同时也为了便于开发者理解NSDate包含的时间信息,通过NSDatedescription方法打印,默认以格里高利历法(Gregorian)下的UTC时间表示NSDate对象的时间。综合以上内容可以发现,NSDate的时间更多的是数据意义上的时间,我们可以称之为“元时间”,与我们平常概念的时间相区别。而在获取元时间后,则是将数据意义上的元时间转为概念意义上的时间。苹果对时间转义过程进行了抽象处理,我们可通过以下几个类方便实现:
NSCalendar,时间历法的处理,默认采用格里高利历法(Gregorian),开发者可以需要,借助苹果提供的其他历法方便实现:

1
NSCalendar objects encapsulate information about systems of reckoning time in which the beginning, length, and divisions of a year are defined. They provide information about the calendar and support for calendrical computations such as determining the range of a given calendrical unit and adding units to a given absolute time.

NSTimeZone,时间地理时区的处理:

1
NSTimeZone is an abstract class that defines the behavior of time zone objects. Time zone objects represent geopolitical regions. Consequently, these objects have names for these regions. Time zone objects also represent a temporal offset, either plus or minus, from Greenwich Mean Time (GMT) and an abbreviation (such as PST for Pacific Standard Time).

NSDateComponents,日期与时间组件:

1
NSDateComponents encapsulates the components of a date in an extendable, object-oriented manner. It is used to specify a date by providing the temporal components that make up a date and time: hour, minutes, seconds, day, month, year, and so on. It can also be used to specify a duration of time, for example, 5 hours and 16 minutes.

NSDateComponentsFormatter,时间日期与时间组件处理:

1
An NSDateComponentsFormatter object takes quantities of time and formats them as a user-readable string. Use a date components formatter to create strings for your app’s interface. The formatter object has many options for creating both abbreviated and expanded strings. The formatter takes the current user’s locale and language into account when generating strings.

NSDateFormatter,时间格式处理:

1
Instances of NSDateFormatter create string representations of NSDate objects, and convert textual representations of dates and times into NSDate objects. For user-visible representations of dates and times, NSDateFormatter provides a variety of localized presets and configuration options. For fixed format representations of dates and times, such as RFC 3339, you can specify a custom format string.

最终,通过转义处理,我们将元时间转换为所需的概念上的时间。
通过对Cocoa框架下,时间处理的理解,具体到将日期转换为对应的星座的实现上,我们依样对实现思路进行一些整理:

1
time -> NSDate -> NSCalendar/NSDateComponents -> Astrological Zodiac Sign

这里先将时间转换为NSDate对象,由于占星星座(十二星座)是采用格里高利历法(Gregorian),因而我们可先保持时间历法的一致性,之后获取与占星星座计算相关的月和日,最后计算并匹配。具体以NSDate分类实现如下:
通过枚举区分占星星座:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef NS_ENUM(NSInteger, NSDateAstrologyZodiacSign) {
NSDateAstrologyZodiacSignAquarius = 0, // [1-20, 2-18] 水瓶座
NSDateAstrologyZodiacSignPisces, // [2-19, 3-20] 双鱼座
NSDateAstrologyZodiacSignAries, // [3-21, 4-19] 白羊座
NSDateAstrologyZodiacSignTaurus, // [4-20, 5-20] 金牛座
NSDateAstrologyZodiacSignGemini, // [5-21, 6-20] 双子座
NSDateAstrologyZodiacSignCancer, // [6-21, 7-22] 巨蟹座
NSDateAstrologyZodiacSignLeo, // [7-23, 8-22] 狮子座
NSDateAstrologyZodiacSignVirgo, // [8-23, 9-22] 处女座
NSDateAstrologyZodiacSignLibra, // [9-23, 10-22] 天秤座
NSDateAstrologyZodiacSignScorpio, // [10-22, 11-21] 天蝎座
NSDateAstrologyZodiacSignSagittarius, // [11-22, 12-21] 射手座
NSDateAstrologyZodiacSignCapricorn // [12-22, 1-19] 摩羯座
};

接下来处理日期与星座匹配问题。观察星座与日期的分布规律,我们可以先将实际问题抽象为数学问题,这里的时间匹配问题可以理解为刻度线条内某点所属的区间问题。具体而言,以星座起始日为刻度,进行区间划分,再逐次判断日期点与刻度值的大小关系,进而确定所属区间,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@property (nonatomic, readonly) NSDateAstrologyZodiacSign zodiacSign;

- (NSDateAstrologyZodiacSign)zodiacSign
{
NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitMonth|NSCalendarUnitDay fromDate:self];

NSInteger temp = components.month * 100 + components.day;

static dispatch_once_t onceToken;
static NSArray<NSNumber *> *signs;
dispatch_once(&onceToken, ^{
signs = @[@120, @219, @321, @420, @521, @621, @723, @823, @923, @1023, @1122, @1222];
});

__block NSDateAstrologyZodiacSign sign = NSDateAstrologyZodiacSignCapricorn;

[signs enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

if (temp < [obj integerValue])
{
if (idx != 0) sign = idx-1;
*stop = YES;
}
}];

return sign;
}

这里,为避免重复创建刻度数组,我们通过GCD一次性代码的办法创建刻度数组,同时通过enumerateObjectsUsingBlock:方法遍历刻度数组,通过数组对象的索引与星座进行关联匹配。至此,如下调用即可:

1
date.zodiacSign;

生肖的处理与星座处理类同,区别主要在于生肖采用中国传统历法(以干支纪年,六十干支年为一个周期,以正月初一为该干支纪年起始),具体实现如下:
通过枚举区分生肖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef NS_ENUM(NSInteger, NSDateAstrologyChineseZodiacSign) {
NSDateAstrologyChineseZodiacSignRat = 0, // 鼠(子)
NSDateAstrologyChineseZodiacSignOx, // 牛(丑)
NSDateAstrologyChineseZodiacSignTiger, // 虎(寅)
NSDateAstrologyChineseZodiacSignRabbit, // 兔(卯)
NSDateAstrologyChineseZodiacSignDragon, // 龙(辰)
NSDateAstrologyChineseZodiacSignSnake, // 蛇(巳)
NSDateAstrologyChineseZodiacSignHorse, // 马(午)
NSDateAstrologyChineseZodiacSignGoat, // 羊(未)
NSDateAstrologyChineseZodiacSignMonkey, // 猴(申)
NSDateAstrologyChineseZodiacSignRooster, // 鸡(酉)
NSDateAstrologyChineseZodiacSignDog, // 狗(戌)
NSDateAstrologyChineseZodiacSignPig // 猪(亥)
};

获取中国传统历法下日期的年,对年进行生肖匹配:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, readonly) NSDateAstrologyChineseZodiacSign chineseZodiacSign;

- (NSDateAstrologyChineseZodiacSign)chineseZodiacSign
{
NSCalendar *chineseCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierChinese];
NSDateComponents *components = [chineseCalendar components:NSCalendarUnitYear fromDate:self];

NSInteger year = components.year;

return (year-1)%12;
}

最后,在需要的地方调用即可:

1
date.chineseZodiacSign;

示例代码:https://github.com/ColinHwang/Demo-of-Tutorial-in-Blog/tree/master/iOS-ConvertDateToAstrologySign

参考资料:

[1]JustBen.iOS时间那点事-NSCalendar + NSDateComponents[EB/OL].http://justben.me/iOS_Time_NSCalender_NSDateComponents ,2013-08-22
[2]Wikipedia.Astrological sign[EB/OL].https://en.wikipedia.org/wiki/Astrological_sign ,2016-9-21.
[3]Wikipedia.生肖[EB/OL].https://zh.wikipedia.org/wiki/%E7%94%9F%E8%82%96 ,2016-9-21.
[4]Apple Inc.NSDate Reference Class[EB/OL].https://developer.apple.com/reference/Foundation/NSDate ,2016-9-21.
[5]Apple Inc.NSCalendar Reference Class[EB/OL].https://developer.apple.com/reference/Foundation/NSCalendar ,2016-9-21.