はじめに

NSDate-Escort というiOS(多分Macでも動く)向けのNSDateに関するユーティリティライブラリを書きました。

CocoaPods 経由でインストール出来ます。

pod 'NSDate-Escort'

どういうライブラリなのかの紹介と、どういう風に開発したかについて書いていきます。

NSDate-Escort

Build Status
Coverage Status
Version
Platform

大部分の機能は NSDate に関わるもので、NSDateのカテゴリとして実装されています。

NSDate-Extensions というNSDateユーティリティライブラリとして有名なものがありますが、
NSDate-EscortNSDate-Extensions と互換性を持ったAPIを提供します。

そのため、NSDate-Utilities.h を読み込んでいる部分を NSDate+Escort.h に置換するだけで、問題なく動作すると思います。

ライセンスは MIT License です。

使い方

使い方は単純で NSDate クラスのカテゴリとして実装されているので、

#import <NSDate+Escort.h>
    
NSDate *today = [NSDate date];
NSDate *nextWeekDate = [today dateByAddingDays:7];// 7日後

という感じで使います。

さすがに全部のAPIを紹介するのは大変なので、詳しくは
NSDate+Escort.h をみるといい気がします。

機能は種類別に分かれていて(と言っても全部NSDate)、種類別で簡単に紹介します。

(selfはカテゴリのsubjectとなるクラス or インスタンスのことを言ってます)

Relative dates from the current date

これだけはNSDateクラスメソッドですが、現在時間からの相対的なNSDateを返します。

Cocoaネイティブには秒を扱う + (id)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs; だけはあるので、分や時間などでも同じようにするためのメソッドです。

#pragma mark - Relative dates from the current date
+ (NSDate *)dateTomorrow;
+ (NSDate *)dateWithDaysFromNow:(NSInteger) dDays;

Comparing dates

selfと渡したNSDateを比較してBOOLを返します

compare:を使って NSComparisonResult の値を見るというややこしい事をしないで書けるようになります。

#pragma mark - Comparing dates
- (BOOL)isEqualToDateIgnoringTime:(NSDate *) otherDate;
- (BOOL)isToday;
- (BOOL)isEarlierThanDate:(NSDate *) aDate;
- (BOOL)isEarlierThanOrEqualDate:(NSDate *) aDate;

Adjusting dates

多分、一番使われると思いますが、self に n日足した値を返したり、selfを00:00:00に、つまり1日の始まりの時間に調節したNSDateを返す等の機能があります。

#pragma mark - Adjusting dates
- (NSDate *)dateByAddingDays:(NSInteger) dDays;
- (NSDate *)dateAtStartOfDay;

この辺を組み合わせると、翌月の月初の00:00:00のNSDateを返すというのも簡単に書くことができます。

[[[aDate dateByAddingMonths:1] dateAtStartOfMonth] dateAtStartOfDay];

Retrieving intervals

self と 渡した NSDateとの相対的な値を返します。

#pragma mark - Retrieving intervals
- (NSInteger)minutesAfterDate:(NSDate *) aDate;

Decomposing dates

self の 時間、分、曜日といった値を返します。
NSDateComponents を使えば同様の事ができますが、NSDateから直接できるようにしている感じです。

#pragma mark - Decomposing dates
@property(readonly) NSInteger hour;
@property(readonly) NSInteger month;

目的

NSDate-Extensions はObjective-Cに馴染みやすいAPIを持っていて便利ですが、使っていると以下のような問題点がありました。

  • テストが書かれていない
  • 無駄な処理が行われている
  • 足りない機能がある

最初はForkしてPull Requestでやろうとしましたが、NSDate-Extensions はそこまで積極的にメンテして修正を取り込んでる感じではなかったのと、

この手の機能は結構良く使うので自分で作りなおしてしまったほうがいいかなと思ったので、NSDate-Escortを作りました。(後、テストを書く練習がしたい症状がでてた)

テストを書く

やっぱり、テストが書かれていないライブラリは不安なので、使い慣れてるKiwiを使ってテストを書きました。

Kiwi で全てのメソッドに対するテストを書いて(後述するカバレッジが念頭にあったため)、QuickCheckが行える箇所についてはNLTQuickCheckを使い、QuickCheckをしています。

この時に、NSDate-Extensionsの実装的な問題点も幾つか見つけたので、NSDate-Escort ではより安全な方向に倒して実装してあります。

例えば、日にちを加算する - (NSDate *) dateByAddingDays: (NSInteger) dDays というメソッドですが、

NSDate-Extensions では NSTimeInterval、つまり秒を加算することで日にちを加算しています。

- (NSDate *) dateByAddingDays: (NSInteger) dDays
{
    NSTimeInterval aTimeInterval = [self timeIntervalSinceReferenceDate] + D_DAY * dDays;
    NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:aTimeInterval];
    return newDate;
}

NSDate-Escort では以下のように、加算用のNSDateComponentsを作り、それを dateByAddingComponents:components することで日にちを加算しています。

- (NSDate *)dateByAddingDays:(NSInteger) dDays {
    NSDateComponents *components = [[NSDateComponents alloc] init];
    components.day = dDays;
    NSCalendar *calendar = [NSDate AZ_currentCalendar];// カレンダーはキャッシュを利用する
    return [calendar dateByAddingComponents:components toDate:self options:0];
}

NSDate-Extensions の実装だと、QuickCheckなどで大きな値を設定した時に
typedef double NSTimeInterval; の範囲を超えることがあります。
(実用的には5000日以上とかなので、あんまり問題にはならないです)

また、Travis CIで動かしていた時に、足していた日にちと1日ずれる現象が発生し、
自分の環境では再現出来なかったのですが、より安全な実装である dateByAddingComponents:components を使った方法を取りました。

Travis CIでテストを動かておくと、普段気づかないことにも気づくチャンスができるので、ライブラリを公開する場合はTravis CIでも動くようにしておくといいと思います。

Build Status

コードカバレッジ

NSDate-Escort の目的の一つにコードカバレッジが100%であることがありました。

これは、書き始めたときはまだ Coveralls でObjective-Cのプロジェクトがひとつも存在してなかっため、Coverallsを使いたいという目的もありました。

NSDate-Escort は NSDate-Extensions 互換のAPIを持つことを目標にしていたので、
最初にインターフェイスだけを書いてコードカバレッジが0%の状態から徐々に上げていく感じで実装して行きました。

コードカバレッジという指標があるとモチベーションを保ち易いように思います。
現在は 100%を達成しているため、 Coverage Status 今後はこれを落とさないようにしていく感じになります。

追加したメソッド

Additional methods?NSDate-Extensions にはなくて、追加したメソッドがまとめてあります。

月単位で、日付を操作する(年単位も追加した)

- (NSDate *)dateByAddingMonths:(NSInteger) dMonths;

dateAtStartOfDay の逆を行う(月や年も同様に)

- (NSDate *)dateAtEndOfDay;

NSDate 同士の >= で比較する

- (BOOL)isEarlierThanOrEqualDate:(NSDate *) aDate;

等が追加してあります。


まとめ

NSDate-Escort を簡単にまとめると

  • NSDate-Extensionsと互換性のあるAPIを持ってる
  • 中身はスクラッチで実装しなおした
  • いくつか追加したメソッドを持ってる
  • 全てのメソッドを通るテストが書かれてる

みたいな感じです。

コントリビューションも歓迎しています。

NSDateについては、Objective-C勉強会@東京 6月 でNSDateについて発表してきた | Web scratchでの発表も見ておくといいかもしれません。(この辺の内容がライブラリにも含まれてます)