Skip to content
This repository has been archived by the owner on May 14, 2018. It is now read-only.

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
c-rack committed Apr 27, 2015
1 parent 010cb10 commit 9a58b5f
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 0 deletions.
26 changes: 26 additions & 0 deletions README.md
@@ -0,0 +1,26 @@
# Quantum

[Cron](https://en.wikipedia.org/wiki/Cron)-like job scheduler for [Elixir](http://elixir-lang.org/) applications.

## Setup

To use this plug in your projects, edit your mix.exs file and add the project as a dependency:

```elixir
defp deps do
[
{ :quantum, ">= 1.0.0" }
]
end
```

## Usage

```elixir
Quantum.cron("0 18-6/2 * * *", fn -> IO.puts("it's late") end)
Quantum.cron("@daily", &backup/0)
```

## License

[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
24 changes: 24 additions & 0 deletions config/config.exs
@@ -0,0 +1,24 @@
# 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 third-
# party users, it should be done in your mix.exs file.

# Sample configuration:
#
# config :logger, :console,
# level: :info,
# format: "$date $time [$level] $metadata$message\n",
# metadata: [:user_id]

# 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"
109 changes: 109 additions & 0 deletions lib/quantum.ex
@@ -0,0 +1,109 @@
defmodule Quantum do
use GenServer
import Process, only: [send_after: 3]

@days ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]
@months ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]

def start_link(options \\ []) do
GenServer.start_link(__MODULE__, %{}, [name: __MODULE__] ++ options)
end

# Public functions ------------------------------------------------------------------------------

def cron("@yearly", fun), do: GenServer.cast(__MODULE__, {"0 0 1 1 *", fun})
def cron("@monthly", fun), do: GenServer.cast(__MODULE__, {"0 0 1 * *", fun})
def cron("@weekly", fun), do: GenServer.cast(__MODULE__, {"0 0 * * 0", fun})
def cron("@daily", fun), do: GenServer.cast(__MODULE__, {"0 0 * * *", fun})
def cron("@hourly", fun), do: GenServer.cast(__MODULE__, {"0 * * * *", fun})
def cron(expression, fun), do: GenServer.cast(__MODULE__, {expression, fun})
def reset, do: GenServer.cast(__MODULE__, :reset)

# Private functions -----------------------------------------------------------------------------

def init(_) do
send_after(self, :tick, 1000)
{:ok, %{jobs: [], d: nil, h: nil, m: nil, w: nil}}
end

def handle_info(:tick, state) do
send_after(self, :tick, 1000)
{d, {h, m, _}} = :calendar.now_to_universal_time(:os.timestamp)
if state.d != d do
state = %{state | w: rem(:calendar.day_of_the_week(d), 7)}
end
if state.m != m do
state = %{state | d: d, h: h, m: m}
Enum.each(state.jobs, fn({e, fun}) -> Task.start(__MODULE__, :execute, [e, fun, state]) end)
end
{:noreply, state}
end
def handle_info(_, state), do: {:noreply, state}

def handle_cast(:reset, state) do
{:noreply, %{state | jobs: []}}
end
def handle_cast({ e, fun }, state) do
{:noreply, %{state | jobs: ["#{e |> String.downcase |> translate}": fun] ++ state.jobs}}
end

def execute("* * * * *", fun, _), do: fun.()
def execute("0 * * * *", fun, %{m: 0}), do: fun.()
def execute("0 0 * * *", fun, %{m: 0, h: 0}), do: fun.()
def execute("0 0 1 * *", fun, %{m: 0, h: 0, d: {_, _, 1}}), do: fun.()
def execute("0 0 1 1 *", fun, %{m: 0, h: 0, d: {_, 1, 1}}), do: fun.()
def execute(e, fun, state) do
[m, h, d, n, w] = e |> String.split(" ")
{_, cur_mon, cur_day} = state.d
cond do
!match(m, state.m, 0, 59) -> false
!match(h, state.h, 0, 59) -> false
!match(d, cur_day, 1, 31) -> false
!match(n, cur_mon, 1, 12) -> false
!match(w, state.w, 0, 6) -> false
true -> fun.()
end
end

defp translate(e) do
{e,_} = List.foldl(@days, {e,0}, fn(x, acc) -> translate(acc, x) end)
{e,_} = List.foldl(@months, {e,1}, fn(x, acc) -> translate(acc, x) end)
e
end
defp translate({e, i}, term), do: {String.replace(e, term, "#{i}"), i+1}

defp match("*", _, _, _), do: true
defp match([], _, _, _), do: false
defp match([e|t], v, min, max), do: Enum.any?(parse(e, min, max), &(&1 == v)) or match(t, v, min, max)
defp match(e, v, min, max), do: match(e |> String.split(","), v, min, max)

defp parse("*/" <> _ = e, min, max) do
[_,i] = e |> String.split("/")
{x,_} = i |> Integer.parse
Enum.reject(min..max, &(rem(&1, x) != 0))
end
defp parse(e, min, max) do
[r|i] = e |> String.split("/")
[x|y] = r |> String.split("-")
{v,_} = x |> Integer.parse
parse(v, y, i, min, max) |> Enum.reject(&((&1 < min) or (&1 > max)))
end
defp parse(v, [], [], _, _), do: [v]
defp parse(v, [], i, _, _) do
{x,_} = i |> Integer.parse
[rem(v,i)]
end
defp parse(v, y, [], min, max) do
{t,_} = y |> Integer.parse
if v < t do
Enum.to_list(v..t)
else
Enum.to_list(v..max) ++ Enum.to_list(min..t)
end
end
defp parse(v, y, i, min, max) do
{x, _} = i |> Integer.parse
parse(v, y, [], min, max) |> Enum.reject(&(rem(&1, x) != 0))
end

end
45 changes: 45 additions & 0 deletions mix.exs
@@ -0,0 +1,45 @@
defmodule Quantum.Mixfile do
use Mix.Project

def project do
[
app: :quantum,
version: "1.0.0",
elixir: "~> 1.0",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps,
description: "Cronjob scheduler for Elixir applications.",
package: package
]
end

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

# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type `mix help deps` for more examples and options
defp deps do
[]
end

defp package do
%{
contributors: ["Constantin Rack"],
licenses: ["Apache License 2.0"],
links: %{"Github" => "https://github.com/c-rack/quantum-elixir"}
}
end

end
20 changes: 20 additions & 0 deletions test/quantum_test.exs
@@ -0,0 +1,20 @@
defmodule QuantumTest do
use ExUnit.Case

test "check hourly" do
Quantum.execute("0 * * * *", fn -> IO.puts("OK") end, %{ d: { 2015, 12, 31 }, h: 12, m: 0, w: 1 } )
end

test "parse */5" do
Quantum.execute("*/5 * * * *", fn -> IO.puts("OK") end, %{ d: { 2015, 12, 31 }, h: 12, m: 0, w: 1 } )
end

test "parse 5" do
Quantum.execute("5 * * * *", fn -> IO.puts("OK") end, %{ d: { 2015, 12, 31 }, h: 12, m: 5, w: 1 } )
end

test "counter example" do
Quantum.execute("5 * * * *", fn -> IO.puts("FAIL") end, %{ d: { 2015, 12, 31 }, h: 12, m: 0, w: 1 } )
end

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

0 comments on commit 9a58b5f

Please sign in to comment.