TimeSteps

A library for handling discrete time series in constant increments. The primary purpose is to describe the time axis when dealing with time series of observational data and climate data.

This library relies heavily on the DateTime class. However, the DateTime class has been deprecated, so please keep that in mind when using this library.

Features

  • TimeStep holds an origin time and a unit time interval.
  • Parsing a time step expression like "hours since 2001-01-01 00:00:00" (originate from udunits library)
  • Obtaining time value for the index value (0 for the origin time)
  • Obtaining index value for the time value
  • Treating non-standard calendar-type such as 'noleap', 'allleap', and '360_day'
  • Comparing the index values between different time step definitions

Installation

gem install timesteps

To use the library in your Ruby script,

require "timesteps"

Description

This library is for time conversion and time index comparison of multiple time series. This library treats the time series data of the type that specifies the time using indexes of time steps since origin time.

Time steps

The main class of this library is the TimeStep class, which holds the origin time and the interval representing the unit time step. For the initialization of the TimeStep object, the following notation is used.

  • "second since 1970-01-01 00:00:00 +00:00"
  • "hour since 2001-01-01 00:00:00 JST"
  • "3 days since 2001-01-01 00:00:00 +00:00"
  • "10 years since 1901-01-01 00:00:00 +00:00"

These notations are imported from Unidata's UDUNITS library. These are also used in CF conventions of NetCDF. However, note that there are some differences between our library and the udunits library about the date-time expressions.

In this library, the elapsed time from the origin is expressed as an index value. For the case of "3 hours since 2001-01-01 00:00:00", the index values are expressed as,

  • "2001-01-01 00:00:00" => 0 (0 / 3 hours) ( 0 days)
  • "2001-01-01 03:00:00" => 1 (1 / 3 hours) ((1/8) days)
  • "2001-01-02 00:00:00" => 8 (8 / 3 hours) ( 1 days)

These are expressed as a Ruby script.

ts = TimeStep.new("3 hours since 2001-01-01 00:00:00")
ts.index_at("2001-01-01 00:00:00")  ### => 0
ts.index_at("2001-01-01 03:00:00")  ### => 1
ts.index_at("2001-01-02 00:00:00")  ### => 8
ts.time_at(0)  ### => #<DateTime 2001-01-01T00:00:00 ...>
ts.time_at(1)  ### => #<DateTime 2001-01-01T03:00:00 ...>
ts.time_at(8)  ### => #<DateTime 2001-01-02T00:00:00 ...>
ts.duration_at(0)  ### => 0      [Integer]
ts.duration_at(1)  ### => (1/8)  [Rational]
ts.duration_at(8)  ### => 1      [Integer] 

Treatment of year and month units

The time units like day, hour, minute, second have constant intervals, but the time units like years and months are not. So, the year and month units are given special treatment in this library. One year is counted at the same month and day as the origin time, and one month is counted on the same day as the origin time.

ts = TimeStep.new("year since 2000-01-15 00:00:00")
ts.index_at("2001-01-14 23:59:59")  ### => 0
ts.index_at("2001-01-15 00:00:00")  ### => 1
ts.index_at("2010-01-14 23:59:59")  ### => 9
ts.index_at("2010-01-15 00:00:00")  ### => 10

ts = TimeStep.new("month since 2000-01-15 00:00:00")
ts.index_at("2000-02-14 23:59:59")  ### => 0
ts.index_at("2000-02-15 00:00:00")  ### => 1
ts.index_at("2000-11-14 23:59:59")  ### => 9
ts.index_at("2000-11-15 00:00:00")  ### => 10

And, it is not possible to give a fractional index for the units of year and month (some methods return a fractional index).

ts = TimeStep.new("year since 2000-01-15 00:00:00")
ts.time_at(0.5) ### => RuntimeError raised
# => in `time_at': index for years should be an integer (RuntimeError)

Calendars

The following calendars, including non-standard calendars, can be handled.

  • standard, gregorian
  • proleptic_gregorian
  • proleptic_julian, julian
  • noleap, 365_day
  • allleap, 366_day
  • 360_day

You can find the description for these calendars at the document of CF-Conventions (4.4.1 Calendar).

ts = TimeStep.new("day since 2000-01-01", calendar: "standard")
ts.time_at(59)  ### => #<DateTime: 2000-02-29T00:00:00+00:00 ...>

ts = TimeStep.new("day since 2000-01-01", calendar: "noleap")
ts.time_at(59)  ### => #<DateTime::NoLeap: 2000-03-01T00:00:00+00:00 ...>

ts = TimeStep.new("day since 2000-01-01", calendar: "360_day")
ts.time_at(59)  ### => #<DateTime::Fixed360Day: 2000-02-30T00:00:00+00:00 ...>

In this library, the DateTime class is adopted as the object that represents date and time (not Time class). Non-standard calendars ("proleptic_gregorian", "julian") can be handled by the DateTime class, with appropriate use of the start parameter. But, other non-standard calendars ("noleap", "allleap", "360_day") can not. So, DateTimeLike class and its subclasses DateTime::NoLeap, DateTime::AllLeap, DateTime::Fixed360Day are introduced. Since many (not all) of methods in the DateTime class are also implemented in DateTimeLike class, users don't need to be too aware of these class differences.

Parsing datetime string

In standard calendar, use DateTime.parse as usual. In other calendars, you can use DateTime.parse_timestamp, which is a particular method to parse a date-time string with a specification of the calendar.

DateTime.parse_timestamp("1200-01-01") ### "standard"
DateTime.parse_timestamp("1200-01-01", calendar: "proleptic_gregorian")
DateTime.parse_timestamp("1200-01-01", calendar: "julian")
DateTime.parse_timestamp("1200-01-01", calendar: "noleap")
DateTime.parse_timestamp("1200-01-01", calendar: "allleap")
DateTime.parse_timestamp("1200-01-01", calendar: "360_day")

If you already have the instance of TimeStep, you can use the method named TimeStep#parse, which is called when it is necessary to convert from the string to the date and time internally.

ts = TimeStep.new("days since 2001-01-01", calendar: "allleap")
ts.parse("2001-02-29") ### => #<DateTime::AllLeap 2001-02-29T ...>

In the UDUNITS library, negative years are treated as BCs, and A.D. 0 is treated as non-existent. This is different from how it is handled in Ruby's DateTime class.

DateTime.parse_timestamp("-0001-01-01") 
   # => #<DateTime: -0001-01-01T00:00:00+00:00 ...>
   # B.C. 2

DateTime.parse_timestamp("0000-01-01") 
   # => #<DateTime: 0000-01-01T00:00:00+00:00 ...>
   # B.C. 1

DateTime.parse_timestamp("BC 0001-01-01") 
   # => #<DateTime: 0000-01-01T00:00:00+00:00 ...>
   # B.C. 1

Comparing multiple time series

TimeStep::Pair is a class to compare indices of two time series, which is initialized by two time step object. It is possible to compute the other index corresponding to the time represented by one of the indices using TimeStep::Pair.

# Create TimeStepPair object
ts1 = TimeStep.new("3 hours since 2001-01-01 21:00:00")
ts2 = TimeStep.new("hour since 2001-01-01 09:00:00")
pair = TimeStep::Pair.new(ts1, ts2)

# You can create same object with,
# pair = TimeStep::Pair.new("3 hours since 2001-01-01 21:00:00", 
#                           "hour since 2001-01-01 09:00:00")

# Forward conversion
pair.forward(0)
   # => 12
pair.forward(2) 
   # => 18        (12 + 2*3h/1h)

# Inverse conversion
pair.inverse(0) 
   # => -4        ((-12h)/3h)
pair.inverse(2) 
   # => (-10/3)   ((-12h+2*1h)/3h)

Examples

Construct a TimeStep object

# standard calendar
ts = TimeStep.new("3 hours since 2001-01-01 09:00:00")

# noleap calendar
ts = TimeStep.new("3 hours since 2001-01-01 09:00:00", calendar: "noleap")

# specify origin time with DateTime object
ts = TimeStep.new("3 hours", since: DateTime.parse("2001-01-01 09:00:00"))

# hourly increments whose origin is the most recent convenient time to the current time
ts = TimeStep.new("1 hour").new_origin(DateTime.now, truncate: true)

# hourly increments whose origin is the most recent convenient time to the current time
# with +0900 time offset
ts = TimeStep.new("1 hour", offset: "+0900").new_origin(DateTime.now, truncate: true)

Attributes of TimeStep object

ts = TimeStep.new("3 hours since 2001-01-01 09:00:00")

# specification
ts.definition
   # => "3 hours since 2001-01-01 09:00:00.000000000 +00:00"
ts.intervalspec
   # => "3 hours"
ts.originspec
   # => "2001-01-01 09:00:00.000000000 +00:00"

# data for time interval
ts.numeric
   # => (3/1)
ts.symbol
   # => :hours
ts.interval
   # => (10800/1)

# origin time
ts.origin
   # => #<DateTime: 2001-01-01T09:00:00+00:00 ((2451911j,32400s,0n),+0s,2299161j)>

# calendar 
ts.calendar
   # => #<TimeStep::Calendar calendar='standard' bc='false'>

What is the index value of the time step 'TS' corresponding to time 'T' ?

# hours
ts = TimeStep.new("6 hours since 2000-01-15 00:00:00")
ts.index_at("2000-01-25 00:00:00")
   # => 40
ts.index_at("2000-01-25 03:00:00")
   # => (81/2)

What is the time corresponding to index 'I' of time step 'TS' ?

# hours
ts = TimeStep.new("6 hours since 2000-01-15 00:00:00")
ts.time_at(40)
   # => #<DateTime: 2000-01-25T00:00:00+00:00 ...>
ts.time_at(40.5)
   # => #<DateTime: 2000-01-25T03:00:00+00:00 ...>

What is the UNIX time stamp for the time corresponding to index 'I' of time step 'TS' ?

ts = TimeStep.new("hour since 2000-01-01 00:00:00")
unixtime = TimeStep.new("seconds since 1970-01-01 00:00:00")
pair = TimeStep::Pair.new(ts, unixtime)

# UNIX time for index 0
pair.forward(0)
   # => 946684800

# UNIX time for index 10
pair.forward(10)
   # => 946720800

How to get each index for a certain time of multiple time steps having different origin times ?

conv = TimeStep::Converter.new("hour since 2000-01-01 00:00:00", name: "ts0")
conv["ts1"] = "hour since 2000-01-02 00:00:00"
conv["ts2"] = "hour since 2000-01-03 00:00:00"
conv["ts3"] = "hour since 2000-01-04 00:00:00"

# index 0 for ts0
conv.forward(0)
   # => {"ts0"=>0, "ts1"=>-24, "ts2"=>-48, "ts3"=>-72}

# index 56 for ts0
conv.forward(56)
   # => {"ts0"=>56, "ts1"=>32, "ts2"=>8, "ts3"=>-16}

# index 81 for ts0
conv.forward(81)
   # => {"ts0"=>81, "ts1"=>57, "ts2"=>33, "ts3"=>9}

# indices [0, 56, 81] for ts0 with time
conv.forward(0, 56, 81, with_time: true)
   # {"time"=>
   #   [#<DateTime: 2000-01-01T00:00:00+00:00 ((2451545j,0s,0n),+0s,2299161j)>,
   #    #<DateTime: 2000-01-03T08:00:00+00:00 ((2451547j,28800s,0n),+0s,2299161j)>,
   #   #<DateTime: 2000-01-04T09:00:00+00:00 ((2451548j,32400s,0n),+0s,2299161j)>],
   #  "ts0"=>[0, 56, 81],
   #  "ts1"=>[-24, 32, 57],
   #  "ts2"=>[-48, 8, 33],
   #  "ts3"=>[-72, -16, 9]}