Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fazibear committed Mar 17, 2016
0 parents commit 14b4093
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
/_build
/cover
/deps
erl_crash.dump
*.ez
20 changes: 20 additions & 0 deletions README.md
@@ -0,0 +1,20 @@
# ExIcal

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:

1. Add ex_ical to your list of dependencies in `mix.exs`:

def deps do
[{:ex_ical, "~> 0.0.1"}]
end

2. Ensure ex_ical is started before your application:

def application do
[applications: [:ex_ical]]
end

30 changes: 30 additions & 0 deletions config/config.exs
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :ex_ical, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:ex_ical, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
116 changes: 116 additions & 0 deletions lib/ex_ical.ex
@@ -0,0 +1,116 @@
defmodule ExIcal do
use Timex

# --- parse ---

def parse(data) do
data |> String.split("\n") |> Enum.reduce([], fn(line, events) ->
parse_line(String.strip(line), events)
end)
end

defp parse_line("BEGIN:VEVENT" <> _, events), do: [%{}] ++ events
defp parse_line("DTSTART" <> start, events) when length(events) > 0, do: events |> put_to_map(:start, process_date(start))
defp parse_line("DTEND" <> endd, events) when length(events) > 0, do: events |> put_to_map(:end, process_date(endd))
defp parse_line("DTSTAMP" <> stamp, events) when length(events) > 0, do: events |> put_to_map(:stamp, process_date(stamp))
defp parse_line("SUMMARY:" <> summary, events) when length(events) > 0, do: events |> put_to_map(:summary, summary)
defp parse_line("DESCRIPTION:" <> description, events) when length(events) > 0, do: events |> put_to_map(:description, description)
defp parse_line("RRULE:" <> rrule, events) when length(events) > 0, do: events |> put_to_map(:rrule, process_rrule(rrule))
defp parse_line(_, events), do: events

defp put_to_map(events, key, value) do
[ event | other ] = events
event = event |> Map.put(key, value)
[event] ++ other
end

defp process_date(":" <> date), do: parse_date(date)
defp process_date(";" <> date) do
[timezone, date] = date |> String.split(":")
timezone = case timezone do
"TZID=" <> timezone -> timezone
_ -> "UTC"
end
parse_date(date, timezone)
end

defp process_rrule(rrule) do
rrule |> String.split(";") |> Enum.reduce(%{}, fn(rule, hash) ->
[key, value] = rule |> String.split("=")
case key |> String.downcase |> String.to_atom do
:until -> hash |> Map.put(:until, parse_date(value))
key -> hash |> Map.put(key, value)
end
end)
end

# --- date parse ---
def parse_date(<< year :: binary-size(4), month :: binary-size(2), day :: binary-size(2), "T",
hour :: binary-size(2), minutes :: binary-size(2), seconds :: binary-size(2), "Z" >>) do
%DateTime{year: year |> String.to_integer, month: month |> String.to_integer, day: day |> String.to_integer,
hour: hour |> String.to_integer, minute: minutes |> String.to_integer, second: seconds |> String.to_integer, timezone: %TimezoneInfo{abbreviation: "UTC"}}
end

def parse_date(<< year :: binary-size(4), month :: binary-size(2), day :: binary-size(2), "Z" >>) do
%DateTime{year: year |> String.to_integer, month: month |> String.to_integer, day: day |> String.to_integer, timezone: %TimezoneInfo{abbreviation: "UTC"}}
end

def parse_date(<< year :: binary-size(4), month :: binary-size(2), day :: binary-size(2), "T",
hour :: binary-size(2), minutes :: binary-size(2), seconds :: binary-size(2) >>, timezone) do
%DateTime{year: year |> String.to_integer, month: month |> String.to_integer, day: day |> String.to_integer,
hour: hour |> String.to_integer, minute: minutes |> String.to_integer, second: seconds |> String.to_integer, timezone: %TimezoneInfo{abbreviation: timezone}}
end

def parse_date(<< year :: binary-size(4), month :: binary-size(2), day :: binary-size(2) >>, timezone) do
%DateTime{year: year |> String.to_integer, month: month |> String.to_integer, day: day |> String.to_integer, timezone: %TimezoneInfo{abbreviation: timezone}}
end

# --- get_events ---

def get_events(events, start_date, end_date) do
(events ++ add_recurring_events(events, end_date)) |> Enum.filter(fn(event) ->
#Date.after?(event[:start], start_date) && Date.before?(event[:start], end_date) && Date.after?(event[:end], start_date) && Date.before?(event[:end], end_date)
true
end)
end

defp add_recurring_events(events, end_date) do
events |> Enum.reduce([], fn(event, revents) ->
case event[:rrule] do
nil ->
revents
_ ->
until = event[:rrule][:until] || end_date
revents ++ (event |> add_recurring_events_for(event[:rrule][:freq], until))
end
end)
end

defp add_recurring_events_for(event, "WEEKLY", until) do
days = (event[:rrule][:interval] || "1") |> String.to_integer

new_event = event
new_event = new_event |> Map.put(:start, Date.shift(event[:start], days: days * 7))
new_event = new_event |> Map.put(:end, Date.shift(event[:end], days: days * 7))

case Date.compare(new_event[:start], until) do
-1 -> [new_event] ++ add_recurring_events_for(new_event, "WEEKLY", until)
0 -> [new_event] ++ add_recurring_events_for(new_event, "WEEKLY", until)
1 -> [new_event]
end
end

defp add_recurring_events_for(event, "DAILY", until) do
days = (event[:rrule][:interval] || "1") |> String.to_integer

new_event = event
new_event = new_event |> Map.put(:start, Date.shift(event[:start], days: days))
new_event = new_event |> Map.put(:end, Date.shift(event[:end], days: days))

case Date.compare(new_event[:start], until) do
-1 -> [new_event] ++ add_recurring_events_for(new_event, "WEEKLY", until)
0 -> [new_event] ++ add_recurring_events_for(new_event, "WEEKLY", until)
1 -> [new_event]
end
end
end
25 changes: 25 additions & 0 deletions mix.exs
@@ -0,0 +1,25 @@
defmodule ExIcal.Mixfile do
use Mix.Project

def project do
[app: :ex_ical,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end

# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: []]
end

defp deps do
[
{:timex, "~> 1.0"}
]
end
end
9 changes: 9 additions & 0 deletions mix.lock
@@ -0,0 +1,9 @@
%{"certifi": {:hex, :certifi, "0.4.0"},
"combine": {:hex, :combine, "0.7.0"},
"hackney": {:hex, :hackney, "1.5.3"},
"idna": {:hex, :idna, "1.2.0"},
"metrics": {:hex, :metrics, "1.0.1"},
"mimerl": {:hex, :mimerl, "1.0.2"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.6"},
"timex": {:hex, :timex, "1.0.2"},
"tzdata": {:hex, :tzdata, "0.5.6"}}
31 changes: 31 additions & 0 deletions test/ex_ical_test.exs
@@ -0,0 +1,31 @@
defmodule ExIcalTest do
use ExUnit.Case
doctest ExIcal

test "parse empty data" do
assert ExIcal.parse("") == []
end

test "event" do
ical = """
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VEVENT
DESCRIPTION:Let's go see Star Wars.
DTEND:20151224T084500Z
DTSTART:20151224T083000Z
SUMMARY:Film with Amy and Adam
END:VEVENT
END:VCALENDAR
"""

event = ExIcal.parse(ical) |> List.first

assert event[:description] == "Let's go see Star Wars."
assert event[:summary] == "Film with Amy and Adam"
assert event[:start] == ExIcal.parse_date("20151224T083000Z")
assert event[:end] == ExIcal.parse_date("20151224T084500Z")
end

end
1 change: 1 addition & 0 deletions test/test_helper.exs
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 14b4093

Please sign in to comment.