ナノ秒単位のタイムスタンプなんて簡単。
そう、C言語ならね。
ただ、Linuxではclock_gettime()
、OS Xではclock_get_time()
と関数名が違う。ナノ秒単位の時間を表す構造体の名前も違う。その構造体を使ってタイムスタンプをゲットする手順も違う。
SwiftはC言語の関数を使うことができるし、せっかくSwiftがLinuxとOS X双方で使えるようになったということもあるし、この違いをSwiftで吸収してみよう。
要約
作ってみた。
そしてGitHubで公開した。
MITライセンスだよ。
ソースコード
ファイルは一つ。ファイル名はTimeSpecification.swiftにしてみた。
そして、Swiftでの構造体の名前はTimeSpecification
にしてみた。プロパティ(stored property)はseconds
とnanoseconds
のみ。
以下、抜粋して説明。説明の順番とソース内のコードの順番は必ずしも一致しないので悪しからず…。
難しいことは何一つしていない。というかできない。
OSの違いを吸収する呪文
OS固有の標準ライブラリを使えるようにimport
する。LinuxならGlibc
、OS XならDarwin
。
ナノ秒単位の時間を表すのはLinuxではstruct timespec
で、OS Xではmach_timespec_t
(typedef struct mach_timespec mach_timespec_t;
)。どちらも秒は.tv_sec
でナノ秒は.tv_nsec
というメンバで表現される。しかし、.tv_sec
はstruct timespec
ではtime_t
だけど、mach_timespec_t
ではunsigned int
1だし、.tv_nsec
はそれぞれlong
、clock_res_t
(typedef int clock_res_t;
)だし、中身は全然違う。
ということで、一旦typealias
でどちらもCTimeSpec
という名前にしちゃってみる。
#if os(Linux)
import Glibc
private typealias CTimeSpec = timespec
#elseif os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
import Darwin
private let mach_task_self:() -> mach_port_t = { return mach_task_self_ }
private typealias CTimeSpec = mach_timespec_t
#else
// UNKNOWN OS
#endif
CTimeSpec
によるTimeSpecification
の初期化は簡単。struct timespec
でもmach_timespec_t
でも、メンバの名前が一緒だからできること:
extension TimeSpecification {
fileprivate init(_ cts:CTimeSpec) {
self.seconds = Int64(cts.tv_sec)
self.nanoseconds = Int32(cts.tv_nsec)
}
}
いざTimeSpecificationの定義
使い道を考えるとComparable
プロトコルを適用しておこう。あとはついでにExpressibleByIntegerLiteral
とExpressibleByFloatLiteral
も適用しておけば便利かなと思って…。
あと、足し算や引き算をしたとき、一つの時間表現は一意に定まるようにnanoseconds
は常に非負整数であるよう正規化するようにしよう。
public struct TimeSpecification: Comparable,
ExpressibleByIntegerLiteral,
ExpressibleByFloatLiteral {
public var seconds:Int64 = 0
public var nanoseconds:Int32 = 0 {
didSet { self.normalize() }
}
public init(seconds:Int64, nanoseconds:Int32) {
self.seconds = seconds
self.nanoseconds = nanoseconds
self.normalize()
}
public mutating func normalize() {
// `nanoseconds` must be always zero or positive value and less than 1_000_000_000
if self.nanoseconds >= 1_000_000_000 {
self.seconds += Int64(self.nanoseconds / 1_000_000_000)
self.nanoseconds = self.nanoseconds % 1_000_000_000
} else if self.nanoseconds < 0 {
// For example,
// (seconds:3, nanoseconds:-2_123_456_789)
// -> (seconds:0, nanoseconds:876_543_211)
self.seconds += Int64(self.nanoseconds / 1_000_000_000) - 1
self.nanoseconds = self.nanoseconds % 1_000_000_000 + 1_000_000_000
}
}
}
どうやってタイムスタンプをゲットするか?
どういう仕様にするか迷ったけど、ここではClock
というenum
を定義して、そのメソッドを使って取得するようにしてみた。たとえば、let now = Clock.System.timeSpecification()
でシステムクロックを取得。
メソッドの中ではC言語のAPIをそのまま使う。どうやら取得には失敗することもあるようなので、一応、Optionalで返すようにした。
なお、OS Xでカレンダークロックを指定するとマイクロ秒単位でしか時間を取得できないみたい。
public enum Clock {
case Calendar
case System
public func timeSpecification() -> TimeSpecification? {
var c_timespec:CTimeSpec = CTimeSpec(tv_sec:0, tv_nsec:0)
let clock_id:CInt
var retval:CInt = -1
#if os(Linux)
clock_id = (self == .Calendar) ? CLOCK_REALTIME : CLOCK_MONOTONIC
retval = clock_gettime(clock_id, &c_timespec)
#elseif os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
var clock_name: clock_serv_t = 0
clock_id = (self == .Calendar) ? CALENDAR_CLOCK : SYSTEM_CLOCK
retval = host_get_clock_service(mach_host_self(), clock_id, &clock_name)
if retval != 0 { return nil }
retval = clock_get_time(clock_name, &c_timespec)
_ = mach_port_deallocate(mach_task_self(), clock_name)
#endif
return (retval == 0) ? TimeSpecification(c_timespec) : nil
}
}
おまけ
どっちかというと、ライブラリを簡単にビルドできるように書いたRubyのスクリプトbuild-install.rbのほうに時間がかかった。実際、SwiftのプロジェクトなのにRubyのコードが4割を占めている。てへっ。