Skip to content

Commit

Permalink
Rewrite for Elixir 1.3
Browse files Browse the repository at this point in the history
  • Loading branch information
bitwalker committed Jul 13, 2016
1 parent 7c736b6 commit 0f6597b
Show file tree
Hide file tree
Showing 97 changed files with 4,564 additions and 5,608 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
language: elixir
sudo: false
elixir:
- 1.2.0
- 1.3
otp_release:
- 18.1
- 19.0
script:
- "MIX_ENV=test mix do deps.get, compile, coveralls.travis"
after_script:
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file (at least to the extent possible, I am not infallible sadly).
This project adheres to [Semantic Versioning](http://semver.org/).

## 3.0.0

**IMPORTANT**: This release is a significant rewrite of Timex's internals as well as API. Many things have remained unchanged,
but there are many things that have as well. Mostly the removal of prior deprecations, and the removal (without deprecation) of
things incompatible with, or now redundant due to, the introduction of calendar types in Elixir 1.3 and their impact on Timex.
The list of these changes will be comprehensively spelled out below, along with recommendations for alternatives in the cases
of removals.

### Fixed
- [#185](https://github.com/bitwalker/timex/issues/185)
- [#137](https://github.com/bitwalker/timex/issues/137)

### Added
- `Timex.Protocol` (defines the API all calendar types must implement to be used with Timex)
- `compare/3`, `diff/3` `shift/2`, now allow the use of `:milliseconds` and `:microseconds`
- `set/2` now allow the use of `:microsecond`
- `Timex.Duration`
- `to_gregorian_microseconds/1`, converts a date/time value to microseconds since year zero

### Changed
- Timex's old Date/DateTime types are replaced by Elixir 1.3's new calendar types,
NaiveDateTime is now used where appropriate, and AmbiguousDateTime remains in order to
handle timezone ambiguities.
- `Timex.diff/3` now returns to it's old behaviour of returning a signed integer for values, so
that diffing/comparing can be done on a single value.
- Renamed `Timex.Time` to `Timex.Duration` to better reflect it's purpose and prevent conflicts with
Elixir's built-in `Time` type.
- Renamed `Timex.Format.Time.*` to `Timex.Formt.Duration.*`
- Renamed `:timestamp` options to `:duration`
- Renamed `*_timestamp` functions to `*_duration`
- Changed `Timex.Duration` to operate on and return `Duration` structs rather than Erlang timestamp tuples
- Changed `Duration.from/2`, to `Duration.from_*/1`, moving the unit into the name.
- Renamed `to_erlang_datetime` to `to_erl`

### Removed
- Timex.Date (use `Timex` now)
- Timex.DateTime (use `Timex` now)
- Timex.Convertable (no longer makes sense in the face of differentiating NaiveDateTime/DateTime)
- `set/2` no longer allow the use of `:millisecond`
- Removed `Timex.date`
- Deprecated `Timex.datetime`
- Removed `from_timestamp` functions
- Removed `to_gregorian`
- Removed `to_seconds/2` in favor of `to_gregorian_seconds/1` and `to_unix/1`
- Removed `normalize/1`, it no longer is necessary. `normalize/2` still exists however

## 2.1.3

### Fixed
Expand Down
134 changes: 48 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ in place of default Erlang types, as well as Ecto types via the `timex_ecto` pac

The complete documentation for Timex is located [here](https://hexdocs.pm/timex).

## Migrating to Timex 2.x
## Migrating to Timex 3.x

See the Migrating section further down for details.
See the Migrating section further down for details. If you are migrating from earlier than 2.x,
please review [this migration doc for 1.x to 2.x](https://github.com/bitwalker/timex/tree/2.2.1#migrating).

Timex 3.0 is a significant rewrite, in order to accomodate Elixir 1.3's new Calendar types in a semantically
correct way. The external API is mostly the same, but there are changes, many without deprecations, so please
read the changelog carefully.

## Getting Started

Expand All @@ -27,7 +32,7 @@ To use Timex with your projects, edit your `mix.exs` file and add it as a depend

```elixir
defp deps do
[{:timex, "~> x.x.x"}]
[{:timex, "~> 3.0"}]
end

defp application do
Expand All @@ -45,69 +50,60 @@ Here's a few simple examples:

```elixir
> use Timex
> date = Date.today
%Date{year: 2016, month: 2, day: 29}
> Timex.today
~D[2016-02-29]

> datetime = Timex.now
#<DateTime(2016-02-29T12:30:30.120+00:00Z Etc/UTC)

> datetime = DateTime.today
%DateTime{year: 2016, month: 2, day: 29,
hour: 12, minute: 30, second: 30, millisecond: 120, timezone: %TimezoneInfo{...}}
> Timex.now("America/Chicago")
#<DateTime(2016-02-29T06:30:30.120-06:00 America/Chicago)

> timestamp = Time.now
{1457, 137754, 906908}
> Duration.now
#<Duration(P46Y6M24DT21H57M33.977711S)>

> {:ok, default_str} = Timex.format(datetime, "{ISO:Extended}")
{:ok, "2016-02-29T12:30:30.120+00:00"}

> {:ok, strftime_str} = Timex.format(datetime, "%FT%T%:z", :strftime)
{:ok, "2016-02-29T12:30:30+00:00"}
> strftime_str = Timex.format!(datetime, "%FT%T%:z", :strftime)
"2016-02-29T12:30:30+00:00"

> Timex.parse(default_str, "{ISO:Extended}")
{:ok, %DateTime{...}}
{:ok, #<DateTime(2016-02-29T12:30:30.120+00:00 Etc/Utc)}

> Timex.parse(strftime_str, "%FT%T%:z", :strftime)
{:ok, %DateTime{...}}
> Timex.parse!(strftime_str, "%FT%T%:z", :strftime)
#<DateTime(2016-02-29T12:30:30.120+00:00 Etc/Utc)

> Time.diff(Time.now, Time.zero, :days)
> Duration.diff(Duration.now, Duration.zero, :days)
16850

> Timex.shift(date, days: 3)
%Date{year: 2016, month: 3, day: 3}
~D[2016-03-03]

> Timex.shift(date, hours: 2, minutes: 13)
%DateTime{year: 2016, month: 2, day: 29,
hour: 14, minute: 43, second: 30, millisecond: 120, timezone: %TimezoneInfo{...}}
> Timex.shift(datetime, hours: 2, minutes: 13)
#<DateTime(2016-02-29T14:43:30.120Z Etc/UTC)>

> timezone = Timex.timezone("America/Chicago", DateTime.today)
%Timex.TimezoneInfo{abbreviation: "CST",
from: {:sunday, {{2015, 11, 1}, {1, 0, 0}}}, full_name: "America/Chicago",
offset_std: 0, offset_utc: -360, until: {:sunday, {{2016, 3, 13}, {2, 0, 0}}}}
> timezone = Timezone.get("America/Chicago", Timex.now)
#<TimezoneInfo(America/Chicago - CDT (-06:00:00))>

> Timezone.convert(datetime, timezone)
%DateTime{year: 2016, month: 2, day: 29,
hour: 6, minute: 30, second: 30, millisecond: 120,
timezone: %TimezoneInfo{abbreviation: "CST", ...}}

> Timex.equal?(Date.today, DateTime.today)
true
#<DateTime(2016-02-29T06:30:30.120-06:00 America/Chicago)>

> Timex.before?(Date.today, Timex.shift(Date.today, days: 1))
> Timex.before?(Timex.today, Timex.shift(Timex.today, days: 1))
true
```

There are a ton of other functions for Dates, Times, and DateTimes, way more than can be covered here. Hopefully the above
gives you a taste of what the API is like!
There are a ton of other functions, all of which work with Erlang datetime tuples, Date, NaiveDateTime, and DateTime. The Duration module contains functions for working with Durations, including Erlang timestamps (such as those returned from `:timer.tc`)

## Extensibility

Timex exposes a number of extension points for you, in order to accomodate different use cases:

You can use custom Date/DateTime types with Timex via the `Timex.Convertable` protocol, which gives you a way to convert your type to various Timex types, and then use the Timex API to manipulate them, for example, you could use the Calendar library's types with Timex via Comparable, or Ecto's, or your own!
Timex itself defines it's core operations on the Date, DateTime, and NaiveDateTime types using the `Timex.Protocol` protocol. From there, all other Timex functionality is derived. If you have custom date/datetime types you want to use with Timex, this is the protocol you would need to implement.

You can compare/diff custom Date/DateTime types with Timex via the `Timex.Comparable` protocol, which also understands types which implement `Timex.Convertable`, allowing you to use Comparable as soon as you've implemented Convertable!
Timex also defines a `Timex.Comparable` protocol, which you can extend to add comparisons to custom date/datetime types.

The same is true for Timex's API in general - if you pass a type which implements `Timex.Convertable`, and the type is not a native Timex one, it will be coerced to one via that protocol.

You can provide your own formatter/parser for Date/DateTime strings by implementing the `Timex.Format.DateTime.Formatter` and/or `Timex.Parse.DateTime.Parser` behaviours, depending on your needs.
You can provide your own formatter/parser for datetime strings by implementing the `Timex.Format.DateTime.Formatter` and/or `Timex.Parse.DateTime.Parser` behaviours, depending on your needs.

## Common Issues

Expand All @@ -119,58 +115,27 @@ You can provide your own formatter/parser for Date/DateTime strings by implement

## Migrating

If you have been using Timex pre-2.x, and you are looking to migrate, it's fairly painless, but important to review the list of breaking
changes and new features.
If you have been using Timex pre-3.x, and you are looking to migrate, it will be fairly painless as long as you review the `CHANGELOG.md` file and make note of anything that has changed which you are using. In almost every single case, the functionality has simply moved, or is accessed slightly differently. In some cases behaviour has changed, but the old behaviour can be acheived manually. For those things which were removed or are no longer available, it is almost certainly because those things are no longer recommended, or no longer make sense for this library. If I have missed something, or there is a good justification for re-adding something which was removed, I'm always open to discussing it on the issue tracker.

### Overview of 2.x changes
### Overview of 3.x changes

Please see the `CHANGELOG.md` file for the list of all changes made, below are a brief recap of the major points, and
instructions on how to migrate your existing Timex-based code to 2.x. I promise it's easy!

- There are now three date types: `Date`, `DateTime`, and `AmbiguousDateTime`. The first two are pretty obvious, but to recap:
- If you are working with dates and don't care about time information - use `Date`
- For everything else, use `DateTime`
- `AmbiguousDateTime` is returned in cases where timezone information is ambiguous for a given point in time. The struct
has two fields `before` and `after`, containing `DateTime` structs to choose from, based on what your intent is. It is up
to you to choose one, or raise an error if you aren't sure what do to.
- To accompany `AmbiguousDateTime` there is also `AmbiguousTimezoneInfo`, which is almost the same thing, except it's fields contain
`TimezoneInfo` structs to choose from. This one is used mostly internally, but if you use `Timezone.get`, you'll need to plan for this.
- All functions which are not specific to a given date type, are now found under the Timex module itself, all functions which
are shared or common between `Date` and `DateTime` can also be found under `Timex` and it will delegate to the appropriate module,
this should make it easier to use `Date` and `DateTime` together without having to remember which API to call for a specific value,
Timex will just do the right thing for you.
- `Timex.Date` and `Timex.DateTime` expose APIs specific to those types, `Timex.DateTime` is effectively the older API you are familiar with from pre-2.x Timex. **Timex.Date is no longer the main API module, use Timex**
- Date/DateTime formatting and parsing APIs are exposed via the `Timex` module, but the old formatter and parser modules are still there,
**the exception being DateFormat, which has been removed, if you were using it, change to Timex**.
- Date/DateTime/Erlang datetime tuple/etc. common conversions are now exposed via the `Timex.Convertable` protocol. Implementations for those types are already included. **Timex.Date.Convert is removed, as well as the DateConvert alias, use Timex.Convertable instead**

There was a significant amount of general project improvements done as part of this release as well:

- Shifting dates/times is now far more accurate, and more flexible than it was previously,
shifting across leaps, timezone changes, and non-existent time periods are now all fully supported
- The API does a much better job of validation and is strict about inputs, and because all APIs now return
error tuples instead of raising exceptions, it is much easier to handle gracefully.
- The code has been reorganized into a more intuitive structure
- Fixed typespecs, docs, and tests across the board
- Almost 100 more tests, with more to come
- Cleaned up dirty code along the way (things like single-piping, inconsistent parens, etc.)
instructions on how to migrate your existing Timex-based code to 3.x.

- Virtually all of Timex's API is consolidated around three modules: `Timex`, `Duration`, and `Interval`. There are public APIs in other modules of course, but most everything you do can be done via one of those three.
- All Timex-provided types have been removed, with the exception of `Time` which has been renamed to `Duration` to better fit it's purpose,
`TimezoneInfo`, which is still used internally, and accessible externally, and `AmbiguousDateTime`, which is still required when dealing with ambiguous `DateTime` structs. `Date` and `DateTime` are now part of Elixir 1.3, and `NaiveDateTime` has been added as a first class type as well.
- Conversions have been rehashed, and are now accessed via the `Timex` module: `to_date`, `to_datetime`, `to_naive_datetime`, `to_erl`, `to_unix`, `to_gregorian_seconds`, and `to_gregorian_microseconds`. Use these to convert back and forth between types. With very few exceptions, there are no `from_*` conversions. You must construct a standard type either with the standard library functions (such as `NaiveDateTime.new`), or by converting from another standard type (i.e. Erlang datetime tuple to NaiveDateTime).

### Migration steps (1.x -> 2.x)
A variety of improvements came about as well:

Depending on how heavily you are using the various features of Timex's API, the migration can be anywhere from 15 minutes to a couple of hours, but the steps below are a guide which should help the process go smoothly. For the vast majority of folks, I anticipate that it will be a very small time investment.

1. Change all `Timex.Date` references to `Timex`, except those which are creating `DateTime` values, such as `Date.now`, those references should be changed to point to `DateTime` now.
2. Change all `DateFormat` references to `Timex`, `DateFormat` was removed.
3. Change all `Timex.Date.Convert` or `DateConvert` references to `Timex` or `Timex.Convertable`, the former have become the latter
4. Make sure you upgrade `timex_ecto` as well if you are using it with your project
5. Compile, if you get warnings about missing methods on `Timex`, they are type-specific functions for `DateTime`,
so change those references to `Timex.DateTime`
6. You'll need to modify your code to handle error tuples instead of exceptions
7. You'll need to handle the new `AmbiguousDateTime` and `AmbiguousTimezoneInfo` structs, the best approach is to pattern match on API return values, use `DateTime` if it was given, or select `:before` or `:after` values from the `Ambiguous*` structs. Your code will become a lot safer as a result of this change!
8. Unit names are soft-deprecated for now, but you'll want to change references to abbreviated units like `secs` to their full names (i.e. `seconds`) in order to make the stderr warnings go away.
- Microsecond precision everywhere it can be maintained
- Various bug fixes from the 2.x release which were held back
- A lot of code clean up
- Faster compilation
- Fixed typespecs, docs, and tests across the board
- Added Julian calendar support

And that's it! If you have any issues migrating, please ping me, and I'll be glad to help. If you have a dependency that uses Timex which you'd like to get updated to 2.x, open an issue here, and I'll submit a PR to those projects to help bring them up to speed quicker.

## Roadmap

Expand All @@ -180,13 +145,10 @@ it, and hopefully get input from the community.

- 100% test coverage (well under way!)
- QuickCheck tests (haven't started this, but I really want to start ASAP)
- Locale-aware formatting/parsing (a relatively high priority)
- `{ASP.NET}` formatting/parsing token for interop with .NET services (probably in the next release)
- Relative time formatter/parser, along the lines of Moment.js's `fromNow`, `toNow`, `timeTo`, and `timeFrom` formatting functions.
- Calendar time formatter/parser, along the lines of Moment.js's calendar time formatter
- Richer duration support via the `Interval` module
- Recurring dates/times API
- Support for calendars other than Gregorian (e.g. Julian)

## License

Expand Down
14 changes: 7 additions & 7 deletions bench/dateformat_bench.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ defmodule Timex.Timex.Bench do

bench "(strftime) parse ISO 8601 datetime" do
datetime = Timex.parse(@datetime, "%FT%TZ", :strftime)
datetime_zoned = Timex.parse(@datetime_zoned, "%FT%T%z", :strftime)
datetime_zoned = Timex.parse(@datetime_zoned, "%FT%T%:z", :strftime)
{:ok, _} = datetime
{:ok, _} = datetime_zoned
end

bench "(default) format ISO 8601 datetime" do
date = DateTime.epoch
{:ok, _} = Timex.format(date, "{ISOz}")
{:ok, _} = Timex.format(date, "{ISO}")
date = Timex.epoch
{:ok, _} = Timex.format(date, "{ISO:Extended:Z}")
{:ok, _} = Timex.format(date, "{ISO:Extended}")
end

bench "(strftime) format ISO 8601 datetime" do
date = DateTime.epoch
date = Timex.epoch
{:ok, _} = Timex.format(date, "%FT%TZ", :strftime)
{:ok, _} = Timex.format(date, "%FT%Tz", :strftime)
end
Expand All @@ -48,8 +48,8 @@ defmodule Timex.Timex.Bench do
{:ok, _} = Default.tokenize("{YYYY}-{M}-{D}T{h24}:{m}:{s}{Z}")
end

bench "DateTime.local" do
_ = DateTime.local
bench "Timex.local" do
_ = Timex.local
:ok
end
end
Loading

0 comments on commit 0f6597b

Please sign in to comment.