From 14b4093b78d0578acb210de434d08661a04f3f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kalbarczyk?= Date: Thu, 17 Mar 2016 23:16:17 +0100 Subject: [PATCH] initial commit --- .gitignore | 5 ++ README.md | 20 ++++++++ config/config.exs | 30 +++++++++++ lib/ex_ical.ex | 116 ++++++++++++++++++++++++++++++++++++++++++ mix.exs | 25 +++++++++ mix.lock | 9 ++++ test/ex_ical_test.exs | 31 +++++++++++ test/test_helper.exs | 1 + 8 files changed, 237 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 lib/ex_ical.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/ex_ical_test.exs create mode 100644 test/test_helper.exs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..755b605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/_build +/cover +/deps +erl_crash.dump +*.ez diff --git a/README.md b/README.md new file mode 100644 index 0000000..84392b3 --- /dev/null +++ b/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 + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..c48df79 --- /dev/null +++ b/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" diff --git a/lib/ex_ical.ex b/lib/ex_ical.ex new file mode 100644 index 0000000..aec21fa --- /dev/null +++ b/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 diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..e1b3ec5 --- /dev/null +++ b/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 diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..af88073 --- /dev/null +++ b/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"}} diff --git a/test/ex_ical_test.exs b/test/ex_ical_test.exs new file mode 100644 index 0000000..6888d41 --- /dev/null +++ b/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 diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()