Giter Club home page Giter Club logo

edtf-ruby's Introduction

EDTF-Ruby

ci Coverage Status

EDTF-Ruby comprises a parser and an API implementation of the Extended Date/Time Format standard.

Compatibility

EDTF-Ruby parser implements all levels and features of the EDTF specification (version September 16, 2011). With the following known caveats:

  • In the latest revision of the EDTF specification alternative versions of partial uncertain/approximate strings were introduced (with or without nested parentheses); EDTF-Ruby currently uses the version that tries to reduce parentheses for printing as we find that one easier to read; the parser accepts all valid dates using this approach, plus some dates using nested expressions (the parser will not accept some of the more complex examples, though).

EDTF-Ruby has been confirmed to work on the following Ruby implementations: 2.1, 2.0, 1.9.3, Rubinius, and JRuby (1.8.7 and 1.9.2 were originally supported but we are not testing compatibility actively anymore). Active Support's date extensions are currently listed as a dependency, because of many functional overlaps (version 3.x and 4.x are supported).

ISO 8601-2

A variation of EDTF will part of the upcoming ISO 8601-2 standard. EDTF-Ruby does not support this new version of EDTF yet, but if you are curious, EDTF.js, an ES6 implementation is already available.

Quickstart

EDTF Ruby is implemented as an extension to the regular Ruby date/time classes. You can parse EDTF strings either using Date.edtf or EDTF.parse (both methods come with an alternative bang! version, that will raise an error if the string cannot be parsed instead of silently returning nil); if given a valid EDTF string the return value will either be an (extended) Date, EDTF::Interval, EDTF::Set, EDTF::Epoch or EDTF::Season instance.

Given any of these instances, you can print the corresponding EDTF string using the #edtf method.

Dates

Most of the EDTF features deal with dates; EDTF-Ruby implements these by extending Active Support's version of the regular Ruby Date class. The library intends to be transparent to Ruby's regular API, i.e., every Date instance should act as you would normally expect but provides additional functionality.

Most, notably, EDTF dates come in day, month, or year precision. This is a subtle difference that determines how many other methods work. For instance:

> Date.today.precision
=> :day
> Date.today
=> Thu, 10 Nov 2011
> Date.today.succ
=> Fri, 11 Nov 2011
> Date.today.month_precision!.succ
=> Sat, 10 Dec 2011

As you can see, dates have day precision by default; after setting the date's precision to month, however, the natural successor is not the next day, but a day a month from now. Always keep precision in mind when comparing dates, too:

> Date.new(1966).year_precision! == Date.new(1966)
=> false

The year 1966 is not equal to the January 1st, 1966. You can set a date's precision directly, or else use the dedicated bang! methods:

> d = Date.new(1993)
> d.day_precision?   # -> true
> d.edtf
=> "1993-01-01"
> d.month_precision!
> d.edtf
=> "1993-01"
> d.year_precision!
> d.edtf
=> "1993"
> d.day_precision?    # -> false
> d.year_precision?   # -> true

In the examples above, you also see that the #edtf method will print a different string depending on the date's current precision.

The second important extension is that dates can be uncertain and or approximate. The distinction between the two may seem somewhat contrived, but we have come to understand it as follows:

Assume you take a history exam and have to answer one of those dreaded questions that require you to say exactly in what year a certain event happened; you studied hard, but all of a sudden you are uncertain: was that the year 1683 or was it 1638? This is what uncertainty is in the parlance of EDTF: in fact, you would write it just like that "1638?".

Approximate dates are similar but slightly different. Lets say you want to tell a story about something that happened to you one winter; you don't recall the exact date, but you know it must have been sometime between Christmas and New Year's. Come to think of it, you don't remember the year either, but you must have been around ten years old. Using EDTF, you could write something like "1993~-12-(27)~": this indicates that both the year and the day are approximations: perhaps the day is not the 27th but it is somewhere close.

This is the main difference between uncertain and approximate in EDTF (in our opinion at least): approximate always means close to the actual number, whilst uncertain could be something completely different (just as there is a large temporal distance between 1638 and 1683).

Here are a few examples of how you can access the uncertain/approximate state of dates in Ruby:

> d = Date.edtf('1984?')
> d.uncertain?
=> true
> d.uncertain? :year
=> true
> d.uncertain? :day
=> false
> d.approximate!
> d.edtf
=> "1984?~"
> d.month_precision!
> d.approximate! :month
> d.edtf
=> "1984?-01~"

As you can see above, you can use the bang! methods to set individual date parts.

In addition, EDTF supports unspecified date parts. EDTF-Ruby provides parsing for both the draft and finalized EDTF specifications, with output meeting the requirements of the final specification:

> d = Date.edtf('1999-03-XX')
> d.unspecified?
=> true
> d.unspecified? :year
=> false
> d.unspecified? :day
=> true
> d.unspecified! :month
> d.edtf
=> "1999-XX-XX"

> draft_d = Date.edtf('1999-03-uu')
> draft_d.edtf
=> "1999-03-XX"

All three, uncertain, approximate, and unspecified attributes do not factor into date calculations (like comparisons or successors etc.).

EDTF long or scientific years are mapped to normal date instances.

> Date.edtf('y-17e7').year
=> -170000000

When printing date strings, EDTF-Ruby will try to avoid nested parentheses:

> Date.edtf("(1999-(02)~-23)?").edtf
=> "1999?-(02)?~-23?"

Intervals

If you parse an EDTF interval, the EDTF-Ruby parser will return an instance of EDTF::Interval; intervals mimic regular Ruby ranges, but offer additional functionality.

> d = Date.edtf('1984-06?/2004-08?')
> d.from.uncertain?
=> true
> d.include?(Date.new(1987,04,13))
=> false

The day is not included because interval has month precision. However:

> d.cover?(Date.new(1987,04,13))
=> true

The day is still covered by the interval. In general, optimized in the same way that Ruby optimizes numeric ranges. Additionally, precision plays into the way intervals are enumerated:

> d.length
=> 243

There are 243 months between 1984-06-01 and 2004-08-31.

> d.step(36).map(&:year)
=> [1984, 1987, 1990, 1993, 1996, 1999, 2002]

Here we iterate through the interval in 36-month steps and map each date to the year.

> Date.edtf('1582-10-01/1582-10-31').length
=> 21

This interval has day precision, so 21 is the number of days in October 1582, which was cut short because of the Gregorian calendar reform.

Intervals can be open or have unknown start or end dates.

> Date.edtf('2004/open').open?
=> true
> Date.edtf('2004/open').cover?(Date.today)
=> true

Sets

EDTF supports two kind of sets: choice lists (meaning one date out of a list), or inclusive lists. In EDTF-Ruby, these are covered by the class EDTF::Set and the choice attribute.

> s = Date.edtf('{1667,1668, 1670..1672}')
> s.choice?
=> false
> s.choice!
> s.edtf
=> "[1667,1668,1670..1672]"

As you can see above, EDTF-Ruby remembers which parts of the set were specified as a range; ranges are however enumerated for membership tests:

> s.include?(Date.edtf('1669'))
=> false
> s.include?(Date.edtf('1671'))
=> true

Even though we're still aware that the year 1671 was is not directly an element of the set:

> s.length
=> 3

When in doubt, you can always map the set to an array. This will also enumerate all ranges:

> s.map(&:year)
=> [1667,1668,1670,1671,1672] # when enumerated there are 5 elements

EDTF sets also feature an #earlier? and #later? attribute:

> s.earlier?
=> false
> s.earlier!
> s.edtf
=> "[..1667,1668,1670..1672]"

Decades and Centuries

The EDTF specification supports so called masked precision strings to define decades or centuries. EDTF-Ruby maps these to dedicated intervals which always cover 10 or 100 years, respectively.

> d = Date.edtf!('196x')
=> 196x
> d.class
=> EDTF::Decade
> d.class
=> EDTF::Decade
> d.min
=> Fri, 01 Jan 1960
> d.max
=> Wed, 31 Dec 1969
> d.map(&:year)
=> [1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969]

Seasons

Finally, EDTF covers seasons. Again, EDTF-Ruby provides a dedicated class for this. Note that EDTF does not make any assumption about the specifics (which months etc.) of the season and you don't have to either; however EDTF-Ruby defines method aliases which allow you to access the seasons by the names spring, summer, autumn (or fall), and winter, respectively. You can also use the more neutral taxonomy of first, second, third, fourth.

> w = Date.edtf!('2003-24')
> w.winter?
=> true
> s = w.succ
> s.spring?
=> true
> s.year
=> 2004
> s.min
=> Mon, 01 Mar 2004
> s.max
=> Mon, 31 May 2004
> s.to_a.length
=> 92
005:0> w.to_a.length
=> 91

As you can see, spring 2004 lasted one day longer than winter 2003 (note that spring and winter here do not relate to the astronomical seasons but strictly to three month periods).

Of course you can print seasons to EDTF strings, too. Finally, seasons can be uncertain/approximate.

> s.edtf
=> "2004-21"
> s.approximate!.edtf
=> "2004-21"

For additional features, please take a look at the documentation or the extensive list of rspec examples.

Contributing

The EDTF-Ruby source code is hosted on GitHub. You can check out a copy of the latest code using Git:

$ git clone https://github.com/inukshuk/edtf-ruby.git

To get started, generate the parser and run all tests:

$ cd edtf-ruby
$ bundle install
$ bundle exec rspec spec
$ bundle exec cucumber

If you've found a bug or have a question, please open an issue on the EDTF-Ruby issue tracker. Or, for extra credit, clone the EDTF-Ruby repository, write a failing example, fix the bug and submit a pull request.

Credits

EDTF-Ruby was written by Sylvester Keil and Ilja Srna.

Published under the terms and conditions of the FreeBSD License; see LICENSE for details.

edtf-ruby's People

Contributors

animosic avatar cbeer avatar dchandekstark avatar inukshuk avatar masaball avatar namyra avatar nicolasleger avatar workergnome avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

edtf-ruby's Issues

Interval comparisons

As discussed in #18 we'd like to reconsider how Intervals are compared if they have an unknown start or an open or unknown end. The current approach is to treat both unknown and open as nil; this leads to potential errors when comparing intervals. See here for the current implementation (min, max, and === are also relevant to this discussion).

Thoughts or PRs welcome : )

Invalid zone offsets added when none given

Parsing a datetime with no zone offset results in a datetime with an offset of +00:00.

EDTF.parse("2001-02-03T09:30:01").edtf
# => "2001-02-03T09:30:01+00:00"

This introduces two problems:

  • ISO 8601 specifies that a zone designator is empty when making use of local time. The implementation encodes the time as UTC instead.
  • The EDTF BNF grammar specifies that 00:00 is an invalid offset, allowing only for minutes 01-59 when the offset hour is 00. The relevant BNF is included below. This may be a mistake in the BNF (it wouldn't be the only one). 8601 is silent on the matter, as far as I can tell.

I propose to omit the zone offset in the object representations & the EDTF output when none is given in the parsed string.

zoneOffset = "Z" 
      | ("+" | "-") 
                     (zoneOffsetHour (":" minute)?
                     | "14:00" 
                     | "00:" oneThru59 ) 

zoneOffsetHour = oneThru13
minute = zeroThru59

oneThru12 = "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12"
oneThru13 = oneThru12 | "13"
oneThru23 = oneThru13 | "14" | "15" | "16" | "17" | "18" | "19" | "20" | "21" | "22" | "23"
zeroThru23 =  "00" | oneThru23
oneThru29 = oneThru23 | "24" | "25" | "26" | "27" | "28" | "29"
oneThru30 = oneThru29 | "30"
oneThru31 = oneThru30 | "31"
oneThru59 = oneThru31 | "32" | "33"| "34"| "35"| "36"| "37"| "38"| "39"| "40"| "41"| "42" | "43"
                | "44"| "45"| "46" | "47"| "48"| "49" |"50"|"51"|"52"|"53"|"54"|"55"|"56"|"57"|"58"|"59"
zeroThru59 = "00" | oneThru59 

Impossible to use unspecified digits in intervals

I found out empirically that you have to use 'x' for unspecified digits so that the date is typed as a century, and 'u' so that it is accepted in an interval: you can't do both at the same time.
So you can't say "from the 19th century to the 20th century."

> Date.edtf('19uu').class
=> Date
> Date.edtf('19xx').class
=> EDTF::Century
> Date.edtf('19xx/2000').class
=> NilClass
> Date.edtf!('19xx/2000').class
ArgumentError (failed to parse date: unexpected '/' at [#<EDTF::Century:0x00005580cf514c80 @year=1900>])
> Date.edtf('19uu/2000').class
=> EDTF::Interval
> Date.edtf('19uu/2000').from.class
=> Date

Implement parse and equality for intervals with open begin dates.

I've run across the issue of not being able to compare intervals that have an open begin dates. In order to test translating date ranges to and from user input fields for start and end, I needed to check that the resulting interval was equal to the expected value for the input. This works well for intervals with a begin date, but does not work when the begin date is left unbounded.

# Open end date intervals are created as expected.
Date.edtf('2015/open')
EDTF::Interval.new(EDTF.parse('2016'), :open)
#<EDTF::Interval:0x00559308b1acd8 @from=Fri, 01 Jan 2016, @to=:open>
#<EDTF::Interval:0x00559308fbc228 @from=Fri, 01 Jan 2016, @to=:open>

# Open begin date intervals are not created as expected.
Date.edtf('open/2012')
EDTF::Interval.new(:open, EDTF.parse('2013'))
# nil
# <EDTF::Interval:0x00559308bfb5d0 @from=:open, @to=Tue, 01 Jan 2013>

# Equality comparison of intervals does not function with open begin dates.
a = EDTF::Interval.new(:open, EDTF.parse('2013'))
b = EDTF::Interval.new(:open, EDTF.parse('2013'))
a == b
#expected true.  Recieved: 
#  ArgumentError: comparison of Date with :open failed
#  .../ruby/gems/2.3.0/gems/edtf-2.3.1/lib/edtf/interval.rb:201:in `<'

Precision of date in set, when attribute later is true

It seems that the precision of date in set is incorrect, when attribute later is true.

irb(main):001:0> s = Date.edtf('[1984,1985-10-01..]')
=> #<EDTF::Set:0x007fe7709a3e90 @dates=#<Set: {Sun, 01 Jan 1984, Tue, 01 Oct 1985}>, @choice=true, @later=true, @earlier=false>

irb(main):002:0> s.entries.map { |date| date.precision }
=> [:year, :year]

Expected values:
=> [:year, :day]

If attribute later is false, the parser works as expected:

irb(main):003:0> s = Date.edtf('[1984,1985-10-01]')
=> #<EDTF::Set:0x007fe770b67c68 @dates=#<Set: {Sun, 01 Jan 1984, Tue, 01 Oct 1985}>, @choice=true, @later=false, @earlier=false>

irb(main):004:0> s.entries.map { |date| date.precision }
=> [:year, :day]

Regression in parsing of 'X' from 3.0.8 to 3.1.1 ?

3.0.8

irb(main):002:0> d = Date.edtf('199X')
=> Mon, 01 Jan 1990..Fri, 31 Dec 1999
irb(main):003:0> d.class
=> EDTF::Decade

3.1.1

irb(main):002:0> d = Date.edtf('199X')
=> Mon, 01 Jan 1990
irb(main):003:0> d.class
=> Date

Parsing sets introduces illegal spaces

The specification doesn't allow for spaces adjacent to the comma separator in sets, but the #edtf method serializes sets with spaces:

EDTF.parse('[1667,1668,1670..1672]').edtf
# => "[1667, 1668, 1670..1672]"

It seems this was corrected in the specification's examples about a year ago, which may be the source of the issue:

(Note: Corrections applied March 18, 2014
Spaces were removed from the EDTF string examples in 5.3.3 and 5.3.4 .

compatibility with ActiveSupport 4

Hi,

I previously wrote with success a gem (edtf-rails) to extend ActiveRecord 3 in order to deal with edtf attribute. Now I'm trying to upgrade it for ActiveRecord 4 but I got a gem incompatibility as edtf depends on ActiveSupport 3.

unspecified masking for BCE years fails

With BCE unspecified years, I'm running into problems setting individual year masks.

For years >0, it works well:

require "edtf"
current = Date.new(300,1,1)
current.edtf # "0300-01-01" 
current.unspecified.year[3]= true 
current.unspecified.year[2]= true
current.edtf #  is  "03uu-01-01", as expected

But for years <0, the masking doesn't work properly:

require "edtf"
past = Date.new(-300,1,1)
past.edtf # "-0300-01-01" 
past.unspecified.year[3]= true 
past.unspecified.year[2]= true
past.edtf #  is "-0uu0-01-01", but should be "-03uu-01-01"

Reordering reversed intervals or do not accept them as valid intervals.

I've come across the situation where I may end up with reversed intervals. That is to say, the start date is greater than the end date. I would expect the behavior of this to either automatically reverse the start and end, or throw an ArgumentError during construction.

Given a start that occurs after the end results in an Interval. The interval just behaves badly.

space_oddessy = Date.edtf('2010/2001')
space_oddessy.begin # nil
space_oddessy.end # nil
space_oddessy.precision # NoMethodError: undefined method `precision' for nil:NilClass

I'm currently handling this at the application level with something like:

    # Determines if an intervals endings are reverse.  Specifically, where the begin 
    # date is later that the end date.  An interval with reversed starting and ending
    # dates will have from and to attributes, but begin and end will be nil
    # == Parameters:
    # interv::
    #   [EDTF::Interval] The interval to be tested. 
    # == Returns:
    #   [Boolean] True if the interval boundaries were reversed
    def self.interval_reversed?(interv)
      return false if interv.from == EDTF_BEGIN_OPEN || interv.to == EDTF_END_OPEN
      (interv.from && !interv.begin) && (interv.to && !interv.end)
    end

    # Reverse the begin and end of an interval.  This will bork a well formed interval.
    # == Parameters:
    # interv::
    #   [EDTF::Interval] The interval to be reversed. 
    # == Returns:
    #   [EDTF::Interval] A new interval with from and to in the reverse order from the input interval.
    def self.reverse_interval(interv)
      EDTF::Interval.new(interv.to, interv.from)
    end

Not sure what the behavior should be: do not accept reversed intervals or reorder reversed intervals.

Typo on docs

Date.new(1666).year_precision! == Date.new(1966)

I think that first one should also be "1966", yes?

open/unknown intervals

Should be reflected by the API. Should be implemented similarly to Ruby's Infinity class for Floats.

Incorrect parsing of unknown date

A completely unknown date can be written as uuuu however this fails to parse:

➜  pncc-data git:(master) ✗ irb
irb(main):001:0> require 'edtf'
=> true
irb(main):002:0> EDTF.parse!('uuuu')
ArgumentError: failed to parse date: unexpected 'u' at []
    from parser.y:473:in `on_error'
    from /Users/barnacle/.rbenv/versions/2.2.4/lib/ruby/2.2.0/racc/parser.rb:258:in `_racc_do_parse_c'
    from /Users/barnacle/.rbenv/versions/2.2.4/lib/ruby/2.2.0/racc/parser.rb:258:in `do_parse'
    from parser.y:469:in `parse!'
    from /Users/barnacle/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/edtf-2.2.0/lib/edtf/date.rb:30:in `edtf!'
    from /Users/barnacle/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/edtf-2.2.0/lib/edtf.rb:83:in `parse!'
    from (irb):2
    from /Users/barnacle/.rbenv/versions/2.2.4/bin/irb:11:in `<main>'

Error in interval with unknown end date

Example:

> d = Date.edtf('2008/unknown')
=> #<EDTF::Interval:0x007fe123862c28 @from=Tue, 01 Jan 2008, @to=:unknown>

> d.min
ArgumentError: comparison of Symbol with Date failed

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.