iOSアプリ開発ではDate型があるのですが、これは日付型のみを扱う事ができません。
そのため「RealmなどのDBから特定日付のレコードを取り出したい」という時に開始日と終了日を指定する必要があって結構めんどくさいです。
class History: Object { @objc dynamic var id = 0 @objc dynamic var date = Date() } let realm = try! Realm() // startDateとendDateで比較するのでめんどくさい let history = realm.objects(History.self).filter("date >= %@ && date < %@", startDate, endDate).first
「dateカラムには常にその日の0:00のデータを入れるというルールにする」って方針もあるのですが、これだと別の国に移動してタイムゾーンがずれた時などにデータがおかしくなる可能性があります。
let history = History() if let date = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()).date { history.date = date }
今回はそういった問題の対策として、Dateを文字列として扱う事で時間情報を省けないかを検討してみます。
Dateを文字列として保存する実装
上で挙げたHistoryオブジェクトのdateカラムを文字列化する場合、下のような実装ができるかと思います。
dateのゲッターとセッターを提供して、その内部ではdateStrカラムからのデータ取得・データの保存を行います。
class History: Object { @objc dynamic var id = 0 @objc dynamic var dateStr = "20180101" var date: Date? { get { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" return formatter.date(from: dateStr) } set { if let date = newValue { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" dateStr = formatter.string(from: date) } } } }
日付のセットは下のように行います。
let history = History() history.date = Date() print(history.date)
特定の日付のレコードの取得は下のように行います。
let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" print(try? Realm().objects(History.self).filter("dateStr = %@", formatter.string(from: Date())).first)
RealmではStringで比較演算子を使う事ができないので、下のように比較する必要があります。
let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { print(try? Realm().objects(History.self).filter { $0.dateStr >= formatter.string(from: startDate) && $0.dateStr < formatter.string(from: endDate) }.count) }
ここまでで取得・保存の実装を見てきましたが少し冗長な書き方になってしまいそうです。
実装のシンプルさという面ではDateを文字列として扱うのはあまり良くないかもしれません。
Dateを文字列として扱う場合のパフォーマンス
次に文字列として扱った場合のパフォーマンスを見てみようと思います。
最初にdateのセットを見ていきます。
下のように100回セットした時の時間を計測します。
端末はiPhone5を利用しました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 let history = History() dates.forEach { date in history.date = date } print(Date().timeIntervalSince1970 - t)
結果は下のとおりです。
100回で0.1秒なので負荷を気にしなくて良いレベルかと思います。
0.108951091766357 // 1回目 0.0738258361816406 // 2回目 0.066342830657959 // 3回目
次は日付の取得を見ていきます。
let histories: [History] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 let history = History() history.date = component.date return history } let t = Date().timeIntervalSince1970 let history = History() histories.forEach { history in _ = history.date } print(Date().timeIntervalSince1970 - t)
結果は下の通りです。
こちらもセットとほぼ同じくらいの速度でした。
0.078549861907959 0.0887308120727539 0.0748848915100098
次にRealmでの検索を見ていきます。
レコードは事前に10000件登録してあります。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" let realm = try? Realm() _ = realm?.objects(History.self).filter("dateStr = %@", formatter.string(from: date)) } print(Date().timeIntervalSince1970 - t)
結果は下の通りです。
日付のセットより少し重いですが気にするほどではなさそうです。
0.303496122360229 0.253895998001099 0.246631860733032
最後に複数データの取得を試してみました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: date) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: date) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { let realm = try? Realm() _ = realm?.objects(History.self).filter { $0.dateStr >= formatter.string(from: startDate) && $0.dateStr <= formatter.string(from: endDate) }.count } } print(Date().timeIntervalSince1970 - t)
これは全レコードを毎回インスタンス化するという事もあって非常に遅かったです。
パフォーメンスの面で言うと文字列でDateを扱うのは難しそうです。
96.0780339241028 92.7221720218658 99.2258989810944
DateをIntとして保存する実装
Dateを文字列で扱うのは、複数データ取得時のパフォーマンス面で厳しそうでした。
代わりにIntとして扱う実装を試してみます。
Intとして扱う場合、Historyテーブルは下のような形の実装が考えられます。
class History: Object { @objc dynamic var id = 0 @objc dynamic var dateValue = 20180101 var date: Date? { get { let year = dateValue / 10000 let month = (dateValue % 10000) / 100 let day = (dateValue % 100) let dateComponent = DateComponents(calendar: Calendar.current, year: year, month: month, day: day) return dateComponent.date } set { if let date = newValue { let year = Calendar.current.component(.year, from: date) let month = Calendar.current.component(.month, from: date) let day = Calendar.current.component(.day, from: date) dateValue = year * 10000 + month * 100 + day } } } }
文字列の時にパフォーマンスが悪かった「複数データ取得」の測定をしてみました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { let startDateValue = Calendar.current.component(.year, from: startDate) * 10000 + Calendar.current.component(.month, from: startDate) * 100 + Calendar.current.component(.day, from: startDate) let endDateValue = Calendar.current.component(.year, from: endDate) * 10000 + Calendar.current.component(.month, from: endDate) * 100 + Calendar.current.component(.day, from: endDate) let realm = try? Realm() _ = realm?.objects(History.self).filter("dateValue >= %d and dateValue <= %d", startDateValue, endDateValue).count } } print(Date().timeIntervalSince1970 - t)
測定結果は下の通りです。
この速度なら採用しても問題なさそうです。
0.374364852905273 0.377358913421631 0.369530916213989
実装においての注意
この実装ですが、和暦の対策が必要です。
和暦対策をしないと、ユーザーの端末設定によってはyearが平成の年数になったりして期待した動きをしない事があります。
常に西暦の年数を取りたい場合は下のような実装をします。
もし毎回初期化する事でパフォーマンスが悪い場合はstatic変数としてCalendarを保持するのもいいかもしれません。
Calendar(identifier: .gregorian).component(.year, from: Date())
まとめ
Dateの日付のみを扱う方法について調べましたが、もし実装するとしたらIntで保持するのが良さそうです。
処理は冗長になりがちなので、その辺りをうまく共通化するともっと便利になりそうです。