defmodule CronstrueEx do @moduledoc """ Converts standard cron expressions into human-readable descriptions. CronstrueEx currently supports five-field cron expressions in English and Spanish. It describes a schedule but does not calculate its next execution time. """ alias CronstrueEx.{Descriptor, ParseError, Parser} alias CronstrueEx.Locales.{En, Es} @default_options [ locale: "en", verbose: false, use_24_hour_time_format: nil, trim_hours_leading_zero: false, throw_exception_on_parse_error: true ] @doc """ Converts a five-field cron expression to a human-readable description. The supported options are: * `:locale` - accepts English (`"en"`/`:en`) or Spanish (`"es"`/`:es`) * `:verbose` - includes wording normally omitted for brevity * `:use_24_hour_time_format` - overrides the locale's default clock format * `:trim_hours_leading_zero` - removes the leading zero from hours * `:throw_exception_on_parse_error` - returns a localized generic error description instead of raising when set to `false` ## Examples iex> CronstrueEx.to_string("* * * * *") "Every minute" iex> CronstrueEx.to_string("*/5 * * * *") "Every 5 minutes" iex> CronstrueEx.to_string("30 11 * * 1-5") "At 11:30 AM, Monday through Friday" iex> CronstrueEx.to_string("30 22 * * *", use_24_hour_time_format: true) "At 22:30" iex> CronstrueEx.to_string("*/5 * * * *", locale: "es") "Cada 5 minutos" """ @spec to_string(String.t(), keyword()) :: String.t() def to_string(expression, options \\ []) do options = normalize_options!(options) case Parser.parse(expression) do {:ok, parsed_expression} -> Descriptor.describe(parsed_expression, options) {:error, message} -> handle_parse_error(message, options) end end defp handle_parse_error(message, options) do if options[:throw_exception_on_parse_error] do raise ParseError, message: message else options[:locale_module].phrase(:parse_error, []) end end defp normalize_options!(options) do unless Keyword.keyword?(options) do raise ArgumentError, "options must be a keyword list" end allowed_options = Keyword.keys(@default_options) unknown_options = Keyword.keys(options) -- allowed_options if unknown_options != [] do raise ArgumentError, "unknown options: #{inspect(unknown_options)}" end normalized = Keyword.merge(@default_options, options) locale_module = locale_module!(normalized[:locale]) for option <- allowed_options -- [:locale, :use_24_hour_time_format] do unless is_boolean(normalized[option]) do raise ArgumentError, "#{inspect(option)} must be a boolean" end end unless is_nil(normalized[:use_24_hour_time_format]) or is_boolean(normalized[:use_24_hour_time_format]) do raise ArgumentError, ":use_24_hour_time_format must be a boolean" end use_24_hour_time_format = if is_nil(normalized[:use_24_hour_time_format]) do locale_module.use_24_hour_time_format_by_default() else normalized[:use_24_hour_time_format] end normalized |> Keyword.put(:locale, locale_name(locale_module)) |> Keyword.put(:locale_module, locale_module) |> Keyword.put(:use_24_hour_time_format, use_24_hour_time_format) end defp locale_module!(locale) when locale in ["en", :en], do: En defp locale_module!(locale) when locale in ["es", :es], do: Es defp locale_module!(locale) do raise ArgumentError, "unsupported locale: #{inspect(locale)}; available locales are English (en) and Spanish (es)" end defp locale_name(En), do: "en" defp locale_name(Es), do: "es" end