Remove unnecessary IEx numbering in Calendar.ISO.valid_date?/2 doc

defmodule Calendar.ISO do
  @moduledoc """
  A calendar implementation that follows to ISO 8601.

  This calendar implements the proleptic Gregorian calendar and
  is therefore compatible with the calendar used in most countries
  today. The proleptic means the Gregorian rules for leap years are
  applied for all time, consequently the dates give different results
  before the year 1583 from when the Gregorian calendar was adopted.

  Note that while ISO 8601 allows times and datetimes to specify
  24:00:00 as the zero hour of the next day, this notation is not
  supported by Elixir.
  """

  @behaviour Calendar

  @unix_epoch 62_167_219_200
  @unix_start 1_000_000 * -@unix_epoch
  @unix_end 315_569_519_999_999_999 - @unix_epoch * 1_000_000
  @unix_range_microseconds @unix_start..@unix_end

  @type year :: 0..9999
  @type month :: 1..12
  @type day :: 1..31

  @seconds_per_minute 60
  @seconds_per_hour 60 * 60
  # Note that this does _not_ handle leap seconds.
  @seconds_per_day 24 * 60 * 60
  @microseconds_per_second 1_000_000
  @parts_per_day @seconds_per_day * @microseconds_per_second

  @days_per_nonleap_year 365
  @days_per_leap_year 366

  @doc """
  Returns the `t:Calendar.iso_days/0` format of the specified date.

  ## Examples

      iex> Calendar.ISO.naive_datetime_to_iso_days(0, 1, 1, 0, 0, 0, {0, 6})
      {0, {0, 86400000000}}
      iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 12, 0, 0, {0, 6})
      {730485, {43200000000, 86400000000}}
      iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 13, 0, 0, {0, 6})
      {730485, {46800000000, 86400000000}}

  """
  @impl true
  @since "1.5.0"
  @spec naive_datetime_to_iso_days(
          Calendar.year(),
          Calendar.month(),
          Calendar.day(),
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: Calendar.iso_days()
  def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
    {date_to_iso_days(year, month, day), time_to_day_fraction(hour, minute, second, microsecond)}
  end

  @doc """
  Converts the `t:Calendar.iso_days/0` format to the datetime format specified by this calendar.

  ## Examples

      iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
      {0, 1, 1, 0, 0, 0, {0, 6}}
      iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
      {2000, 1, 1, 0, 0, 0, {0, 6}}
      iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
      {2000, 1, 1, 12, 0, 0, {0, 6}}

  """
  @spec naive_datetime_from_iso_days(Calendar.iso_days()) :: {
          Calendar.year(),
          Calendar.month(),
          Calendar.day(),
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        }
  @impl true
  @since "1.5.0"
  def naive_datetime_from_iso_days({days, day_fraction}) do
    {year, month, day} = date_from_iso_days(days)
    {hour, minute, second, microsecond} = time_from_day_fraction(day_fraction)
    {year, month, day, hour, minute, second, microsecond}
  end

  @doc """
  Returns the normalized day fraction of the specified time.

  ## Examples

      iex> Calendar.ISO.time_to_day_fraction(0, 0, 0, {0, 6})
      {0, 86400000000}
      iex> Calendar.ISO.time_to_day_fraction(12, 34, 56, {123, 6})
      {45296000123, 86400000000}

  """
  @impl true
  @since "1.5.0"
  @spec time_to_day_fraction(
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: Calendar.day_fraction()
  def time_to_day_fraction(0, 0, 0, {0, _}) do
    {0, @parts_per_day}
  end

  def time_to_day_fraction(hour, minute, second, {microsecond, _}) do
    combined_seconds = hour * @seconds_per_hour + minute * @seconds_per_minute + second
    {combined_seconds * @microseconds_per_second + microsecond, @parts_per_day}
  end

  @doc """
  Converts a day fraction to this Calendar's representation of time.

  ## Examples

      iex> Calendar.ISO.time_from_day_fraction({1, 2})
      {12, 0, 0, {0, 6}}
      iex> Calendar.ISO.time_from_day_fraction({13, 24})
      {13, 0, 0, {0, 6}}

  """
  @impl true
  @since "1.5.0"
  @spec time_from_day_fraction(Calendar.day_fraction()) ::
          {Calendar.hour(), Calendar.minute(), Calendar.second(), Calendar.microsecond()}
  def time_from_day_fraction({parts_in_day, parts_per_day}) do
    total_microseconds = div(parts_in_day * @parts_per_day, parts_per_day)

    {hours, rest_microseconds1} =
      div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)

    {minutes, rest_microseconds2} =
      div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)

    {seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
    {hours, minutes, seconds, {microseconds, 6}}
  end

  # Converts year, month, day to count of days since 0000-01-01.
  @doc false
  @since "1.5.0"
  def date_to_iso_days(0, 1, 1) do
    0
  end

  def date_to_iso_days(1970, 1, 1) do
    719_528
  end

  def date_to_iso_days(year, month, day) when year in 0..9999 do
    true = day <= days_in_month(year, month)

    days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
      1
  end

  # Converts count of days since 0000-01-01 to {year, month, day} tuple.
  @doc false
  @since "1.5.0"
  def date_from_iso_days(days) when days in 0..3_652_424 do
    {year, day_of_year} = days_to_year(days)
    extra_day = if leap_year?(year), do: 1, else: 0
    {month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
    {year, month, day_in_month + 1}
  end

  defp div_mod(int1, int2) do
    div = div(int1, int2)
    mod = int1 - div * int2
    {div, mod}
  end

  @doc """
  Returns how many days there are in the given year-month.

  ## Examples

      iex> Calendar.ISO.days_in_month(1900, 1)
      31
      iex> Calendar.ISO.days_in_month(1900, 2)
      28
      iex> Calendar.ISO.days_in_month(2000, 2)
      29
      iex> Calendar.ISO.days_in_month(2001, 2)
      28
      iex> Calendar.ISO.days_in_month(2004, 2)
      29
      iex> Calendar.ISO.days_in_month(2004, 4)
      30

  """
  @spec days_in_month(year, month) :: 28..31
  @impl true
  def days_in_month(year, month)

  def days_in_month(year, 2) do
    if leap_year?(year), do: 29, else: 28
  end

  def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
  def days_in_month(_, month) when month in 1..12, do: 31

  @doc """
  Returns if the given year is a leap year.

  ## Examples

      iex> Calendar.ISO.leap_year?(2000)
      true
      iex> Calendar.ISO.leap_year?(2001)
      false
      iex> Calendar.ISO.leap_year?(2004)
      true
      iex> Calendar.ISO.leap_year?(1900)
      false

  """
  @spec leap_year?(year) :: boolean()
  @impl true
  def leap_year?(year) when is_integer(year) and year >= 0 do
    rem(year, 4) === 0 and (rem(year, 100) > 0 or rem(year, 400) === 0)
  end

  @doc """
  Calculates the day of the week from the given `year`, `month`, and `day`.

  It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.

  ## Examples

      iex> Calendar.ISO.day_of_week(2016, 10, 31)
      1
      iex> Calendar.ISO.day_of_week(2016, 11, 01)
      2
      iex> Calendar.ISO.day_of_week(2016, 11, 02)
      3
      iex> Calendar.ISO.day_of_week(2016, 11, 03)
      4
      iex> Calendar.ISO.day_of_week(2016, 11, 04)
      5
      iex> Calendar.ISO.day_of_week(2016, 11, 05)
      6
      iex> Calendar.ISO.day_of_week(2016, 11, 06)
      7
  """
  @spec day_of_week(year, month, day) :: 1..7
  @impl true
  def day_of_week(year, month, day)
      when is_integer(year) and is_integer(month) and is_integer(day) do
    Integer.mod(date_to_iso_days(year, month, day) + 5, 7) + 1
  end

  @doc """
  Converts the given time into a string.

  ## Examples

      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 6})
      "02:02:02.000002"
      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 2})
      "02:02:02.00"
      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 0})
      "02:02:02"
  """
  @spec time_to_string(
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: String.t()
  @impl true
  def time_to_string(hour, minute, second, microsecond) do
    time_to_string(hour, minute, second, microsecond, :extended)
  end

  def time_to_string(hour, minute, second, {_, 0}, format) do
    time_to_string_format(hour, minute, second, format)
  end

  def time_to_string(hour, minute, second, {microsecond, precision}, format) do
    time_to_string_format(hour, minute, second, format) <>
      "." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
  end

  defp time_to_string_format(hour, minute, second, :extended) do
    zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
  end

  defp time_to_string_format(hour, minute, second, :basic) do
    zero_pad(hour, 2) <> zero_pad(minute, 2) <> zero_pad(second, 2)
  end

  @doc """
  Converts the given date into a string.

  ## Examples

    iex> Calendar.ISO.date_to_string(2015, 2, 28)
    "2015-02-28"
    iex> Calendar.ISO.date_to_string(2017, 8, 1)
    "2017-08-01"
  """
  @spec date_to_string(year, month, day) :: String.t()
  @impl true
  def date_to_string(year, month, day) do
    date_to_string(year, month, day, :extended)
  end

  defp date_to_string(year, month, day, :extended) do
    zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
  end

  defp date_to_string(year, month, day, :basic) do
    zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
  end

  @doc """
  Converts the datetime (without time zone) into a string.

  ## Examples

    iex> Calendar.ISO.naive_datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 6})
    "2015-02-28 01:02:03.000004"
    iex> Calendar.ISO.naive_datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5})
    "2017-08-01 01:02:03.00000"
  """
  @impl true
  @spec naive_datetime_to_string(
          year,
          month,
          day,
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: String.t()
  def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
    date_to_string(year, month, day) <> " " <> time_to_string(hour, minute, second, microsecond)
  end

  @doc """
  Converts the datetime (with time zone) into a string.

  ## Examples

    iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, "Europe/Berlin", "CET", 3600, 0)
    "2017-08-01 01:02:03.00000+01:00 CET Europe/Berlin"
    iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, "Europe/Berlin", "CDT", 3600, 3600)
    "2017-08-01 01:02:03.00000+02:00 CDT Europe/Berlin"
    iex> Calendar.ISO.datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 5}, "America/Los_Angeles", "PST", -28800, 0)
    "2015-02-28 01:02:03.00000-08:00 PST America/Los_Angeles"
    iex> Calendar.ISO.datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 5}, "America/Los_Angeles", "PDT", -28800, 3600)
    "2015-02-28 01:02:03.00000-07:00 PDT America/Los_Angeles"
  """
  @impl true
  @spec datetime_to_string(
          year,
          month,
          day,
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond(),
          Calendar.time_zone(),
          Calendar.zone_abbr(),
          Calendar.utc_offset(),
          Calendar.std_offset()
        ) :: String.t()
  def datetime_to_string(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        time_zone,
        zone_abbr,
        utc_offset,
        std_offset
      ) do
    date_to_string(year, month, day) <>
      " " <>
      time_to_string(hour, minute, second, microsecond) <>
      offset_to_string(utc_offset, std_offset, time_zone) <>
      zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
  end

  @doc """
  Determines if the date given is valid according to the proleptic Gregorian calendar.

  ## Examples

    iex> Calendar.ISO.valid_date?(2015, 2, 28)
    true
    iex> Calendar.ISO.valid_date?(2015, 2, 30)
    false

  """
  @impl true
  @since "1.5.0"
  @spec valid_date?(year, month, day) :: boolean
  def valid_date?(year, month, day) do
    month in 1..12 and year in 0..9999 and day in 1..days_in_month(year, month)
  end

  @doc """
  Determines if the date given is valid according to the proleptic Gregorian calendar.
  Note that leap seconds are considered valid, but the use of 24:00:00 as the
  zero hour of the day is considered invalid.

  ## Examples

    iex> Calendar.ISO.valid_time?(10, 50, 25, {3006, 6})
    true
    iex> Calendar.ISO.valid_time?(23, 59, 60, {0, 0})
    true
    iex> Calendar.ISO.valid_time?(24, 0, 0, {0, 0})
    false
  """
  @impl true
  @since "1.5.0"
  @spec valid_time?(Calendar.hour(), Calendar.minute(), Calendar.secon(), Calendar.microsecond()) ::
          boolean
  def valid_time?(hour, minute, second, {microsecond, precision}) do
    hour in 0..23 and minute in 0..59 and second in 0..60 and microsecond in 0..999_999 and
      precision in 0..6
  end

  @doc """
  See `c:Calendar.day_rollover_relative_to_midlight_utc/0` for documentation.
  """
  @impl true
  @since "1.5.0"
  @spec day_rollover_relative_to_midnight_utc() :: {0, 1}
  def day_rollover_relative_to_midnight_utc() do
    {0, 1}
  end

  defp offset_to_string(utc, std, zone, format \\ :extended)
  defp offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"

  defp offset_to_string(utc, std, _zone, format) do
    total = utc + std
    second = abs(total)
    minute = second |> rem(3600) |> div(60)
    hour = div(second, 3600)
    format_offset(total, hour, minute, format)
  end

  defp format_offset(total, hour, minute, :extended) do
    sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
  end

  defp format_offset(total, hour, minute, :basic) do
    sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
  end

  defp zone_to_string(0, 0, _abbr, "Etc/UTC"), do: ""
  defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone

  defp sign(total) when total < 0, do: "-"
  defp sign(_), do: "+"

  defp zero_pad(val, count) do
    num = Integer.to_string(val)
    :binary.copy("0", count - byte_size(num)) <> num
  end

  ## Helpers

  @doc false
  def from_unix(integer, unit) when is_integer(integer) do
    total = System.convert_time_unit(integer, unit, :microsecond)

    if total in @unix_range_microseconds do
      microseconds = Integer.mod(total, @microseconds_per_second)
      seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
      precision = precision_for_unit(unit)
      {date, time} = iso_seconds_to_datetime(seconds)
      {:ok, date, time, {microseconds, precision}}
    else
      {:error, :invalid_unix_time}
    end
  end

  defp precision_for_unit(unit) do
    subsecond = div(System.convert_time_unit(1, :second, unit), 10)
    precision_for_unit(subsecond, 0)
  end

  defp precision_for_unit(0, precision), do: precision
  defp precision_for_unit(_, 6), do: 6

  defp precision_for_unit(number, precision),
    do: precision_for_unit(div(number, 10), precision + 1)

  @doc false
  @since "1.5.0"
  def date_to_iso8601(year, month, day, format \\ :extended) do
    date_to_string(year, month, day, format)
  end

  @doc false
  def time_to_iso8601(hour, minute, second, microsecond, format \\ :extended) do
    time_to_string(hour, minute, second, microsecond, format)
  end

  @doc false
  def naive_datetime_to_iso8601(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        format \\ :extended
      ) do
    date_to_string(year, month, day, format) <>
      "T" <> time_to_string(hour, minute, second, microsecond, format)
  end

  @doc false
  def datetime_to_iso8601(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        time_zone,
        _zone_abbr,
        utc_offset,
        std_offset,
        format \\ :extended
      ) do
    date_to_string(year, month, day, format) <>
      "T" <>
      time_to_string(hour, minute, second, microsecond, format) <>
      offset_to_string(utc_offset, std_offset, time_zone, format)
  end

  @doc false
  def parse_microsecond("." <> rest) do
    case parse_microsecond(rest, 0, "") do
      {"", 0, _} ->
        :error

      {microsecond, precision, rest} when precision in 1..6 ->
        pad = String.duplicate("0", 6 - byte_size(microsecond))
        {{String.to_integer(microsecond <> pad), precision}, rest}

      {microsecond, _precision, rest} ->
        {{String.to_integer(binary_part(microsecond, 0, 6)), 6}, rest}
    end
  end

  def parse_microsecond("," <> rest) do
    parse_microsecond("." <> rest)
  end

  def parse_microsecond(rest) do
    {{0, 0}, rest}
  end

  defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
    do: parse_microsecond(tail, precision + 1, <<acc::binary, head>>)

  defp parse_microsecond(rest, precision, acc), do: {acc, precision, rest}

  @doc false
  def parse_offset(""), do: {nil, ""}
  def parse_offset("Z"), do: {0, ""}
  def parse_offset("-00:00"), do: :error

  def parse_offset(<<?+, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
    do: parse_offset(1, hour, min, rest)

  def parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
    do: parse_offset(-1, hour, min, rest)

  def parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
    do: parse_offset(1, hour, min, rest)

  def parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
    do: parse_offset(-1, hour, min, rest)

  def parse_offset(<<?+, hour::2-bytes, rest::binary>>), do: parse_offset(1, hour, "00", rest)
  def parse_offset(<<?-, hour::2-bytes, rest::binary>>), do: parse_offset(-1, hour, "00", rest)
  def parse_offset(_), do: :error

  defp parse_offset(sign, hour, min, rest) do
    with {hour, ""} when hour < 24 <- Integer.parse(hour),
         {min, ""} when min < 60 <- Integer.parse(min) do
      {(hour * 60 + min) * 60 * sign, rest}
    else
      _ -> :error
    end
  end

  @doc false
  @since "1.5.0"
  def iso_days_to_unit({days, {parts, ppd}}, unit) do
    day_microseconds = days * @parts_per_day
    microseconds = div(parts * @parts_per_day, ppd)
    System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
  end

  @doc false
  @since "1.5.0"
  def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, ppd) do
    normalize_iso_days(days, parts + add, ppd)
  end

  def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, add_ppd) do
    parts = parts * add_ppd
    add = add * ppd
    gcd = Integer.gcd(ppd, add_ppd)
    result_parts = div(parts + add, gcd)
    result_ppd = div(ppd * add_ppd, gcd)
    normalize_iso_days(days, result_parts, result_ppd)
  end

  defp normalize_iso_days(days, parts, ppd) do
    days_offset = div(parts, ppd)
    parts = rem(parts, ppd)

    if parts < 0 do
      {days + days_offset - 1, {parts + ppd, ppd}}
    else
      {days + days_offset, {parts, ppd}}
    end
  end

  # Note that this function does not add the extra leap day for a leap year.
  # If you want to add that leap day when appropriate,
  # add the result of leap_day_offset/2 to the result of days_before_month/1.
  defp days_before_month(1), do: 0
  defp days_before_month(2), do: 31
  defp days_before_month(3), do: 59
  defp days_before_month(4), do: 90
  defp days_before_month(5), do: 120
  defp days_before_month(6), do: 151
  defp days_before_month(7), do: 181
  defp days_before_month(8), do: 212
  defp days_before_month(9), do: 243
  defp days_before_month(10), do: 273
  defp days_before_month(11), do: 304
  defp days_before_month(12), do: 334

  defp leap_day_offset(_year, month) when month < 3, do: 0

  defp leap_day_offset(year, _month) do
    if leap_year?(year), do: 1, else: 0
  end

  defp days_to_year(days) do
    year = Integer.floor_div(days, @days_per_nonleap_year)
    {year, days_before_year} = days_to_year(year, days, days_in_previous_years(year))
    {year, days - days_before_year}
  end

  defp days_to_year(year, days1, days2) when days1 < days2 do
    days_to_year(year - 1, days1, days_in_previous_years(year - 1))
  end

  defp days_to_year(year, _days1, days2) do
    {year, days2}
  end

  defp days_in_previous_years(0), do: 0

  defp days_in_previous_years(year) do
    previous_year = year - 1

    Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
      Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
      @days_per_leap_year
  end

  # Note that 0 is the first day of the month.
  defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
    {1, day_of_year}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
    {2, day_of_year - 31}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
    {3, day_of_year - (59 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
    {4, day_of_year - (90 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
    {5, day_of_year - (120 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
    {6, day_of_year - (151 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
    {7, day_of_year - (181 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
    {8, day_of_year - (212 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
    {9, day_of_year - (243 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
    {10, day_of_year - (273 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
    {11, day_of_year - (304 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) do
    {12, day_of_year - (334 + extra_day)}
  end

  defp iso_seconds_to_datetime(seconds) do
    {days, rest_seconds} = div_mod(seconds, @seconds_per_day)

    date = date_from_iso_days(days)
    time = seconds_to_time(rest_seconds)
    {date, time}
  end

  defp seconds_to_time(seconds) when seconds in 0..(@seconds_per_day - 1) do
    {hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
    {minute, second} = div_mod(rest_seconds, @seconds_per_minute)

    {hour, minute, second}
  end
end

defmodule Calendar.ISO do
  @moduledoc """
  A calendar implementation that follows to ISO 8601.

  This calendar implements the proleptic Gregorian calendar and
  is therefore compatible with the calendar used in most countries
  today. The proleptic means the Gregorian rules for leap years are
  applied for all time, consequently the dates give different results
  before the year 1583 from when the Gregorian calendar was adopted.

  Note that while ISO 8601 allows times and datetimes to specify
  24:00:00 as the zero hour of the next day, this notation is not
  supported by Elixir.
  """

  @behaviour Calendar

  @unix_epoch 62_167_219_200
  @unix_start 1_000_000 * -@unix_epoch
  @unix_end 315_569_519_999_999_999 - @unix_epoch * 1_000_000
  @unix_range_microseconds @unix_start..@unix_end

  @type year :: 0..9999
  @type month :: 1..12
  @type day :: 1..31

  @seconds_per_minute 60
  @seconds_per_hour 60 * 60
  # Note that this does _not_ handle leap seconds.
  @seconds_per_day 24 * 60 * 60
  @microseconds_per_second 1_000_000
  @parts_per_day @seconds_per_day * @microseconds_per_second

  @days_per_nonleap_year 365
  @days_per_leap_year 366

  @doc """
  Returns the `t:Calendar.iso_days/0` format of the specified date.

  ## Examples

      iex> Calendar.ISO.naive_datetime_to_iso_days(0, 1, 1, 0, 0, 0, {0, 6})
      {0, {0, 86400000000}}
      iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 12, 0, 0, {0, 6})
      {730485, {43200000000, 86400000000}}
      iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 13, 0, 0, {0, 6})
      {730485, {46800000000, 86400000000}}

  """
  @impl true
  @since "1.5.0"
  @spec naive_datetime_to_iso_days(
          Calendar.year(),
          Calendar.month(),
          Calendar.day(),
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: Calendar.iso_days()
  def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
    {date_to_iso_days(year, month, day), time_to_day_fraction(hour, minute, second, microsecond)}
  end

  @doc """
  Converts the `t:Calendar.iso_days/0` format to the datetime format specified by this calendar.

  ## Examples

      iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
      {0, 1, 1, 0, 0, 0, {0, 6}}
      iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
      {2000, 1, 1, 0, 0, 0, {0, 6}}
      iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
      {2000, 1, 1, 12, 0, 0, {0, 6}}

  """
  @spec naive_datetime_from_iso_days(Calendar.iso_days()) :: {
          Calendar.year(),
          Calendar.month(),
          Calendar.day(),
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        }
  @impl true
  @since "1.5.0"
  def naive_datetime_from_iso_days({days, day_fraction}) do
    {year, month, day} = date_from_iso_days(days)
    {hour, minute, second, microsecond} = time_from_day_fraction(day_fraction)
    {year, month, day, hour, minute, second, microsecond}
  end

  @doc """
  Returns the normalized day fraction of the specified time.

  ## Examples

      iex> Calendar.ISO.time_to_day_fraction(0, 0, 0, {0, 6})
      {0, 86400000000}
      iex> Calendar.ISO.time_to_day_fraction(12, 34, 56, {123, 6})
      {45296000123, 86400000000}

  """
  @impl true
  @since "1.5.0"
  @spec time_to_day_fraction(
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: Calendar.day_fraction()
  def time_to_day_fraction(0, 0, 0, {0, _}) do
    {0, @parts_per_day}
  end

  def time_to_day_fraction(hour, minute, second, {microsecond, _}) do
    combined_seconds = hour * @seconds_per_hour + minute * @seconds_per_minute + second
    {combined_seconds * @microseconds_per_second + microsecond, @parts_per_day}
  end

  @doc """
  Converts a day fraction to this Calendar's representation of time.

  ## Examples

      iex> Calendar.ISO.time_from_day_fraction({1, 2})
      {12, 0, 0, {0, 6}}
      iex> Calendar.ISO.time_from_day_fraction({13, 24})
      {13, 0, 0, {0, 6}}

  """
  @impl true
  @since "1.5.0"
  @spec time_from_day_fraction(Calendar.day_fraction()) ::
          {Calendar.hour(), Calendar.minute(), Calendar.second(), Calendar.microsecond()}
  def time_from_day_fraction({parts_in_day, parts_per_day}) do
    total_microseconds = div(parts_in_day * @parts_per_day, parts_per_day)

    {hours, rest_microseconds1} =
      div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)

    {minutes, rest_microseconds2} =
      div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)

    {seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
    {hours, minutes, seconds, {microseconds, 6}}
  end

  # Converts year, month, day to count of days since 0000-01-01.
  @doc false
  @since "1.5.0"
  def date_to_iso_days(0, 1, 1) do
    0
  end

  def date_to_iso_days(1970, 1, 1) do
    719_528
  end

  def date_to_iso_days(year, month, day) when year in 0..9999 do
    true = day <= days_in_month(year, month)

    days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
      1
  end

  # Converts count of days since 0000-01-01 to {year, month, day} tuple.
  @doc false
  @since "1.5.0"
  def date_from_iso_days(days) when days in 0..3_652_424 do
    {year, day_of_year} = days_to_year(days)
    extra_day = if leap_year?(year), do: 1, else: 0
    {month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
    {year, month, day_in_month + 1}
  end

  defp div_mod(int1, int2) do
    div = div(int1, int2)
    mod = int1 - div * int2
    {div, mod}
  end

  @doc """
  Returns how many days there are in the given year-month.

  ## Examples

      iex> Calendar.ISO.days_in_month(1900, 1)
      31
      iex> Calendar.ISO.days_in_month(1900, 2)
      28
      iex> Calendar.ISO.days_in_month(2000, 2)
      29
      iex> Calendar.ISO.days_in_month(2001, 2)
      28
      iex> Calendar.ISO.days_in_month(2004, 2)
      29
      iex> Calendar.ISO.days_in_month(2004, 4)
      30

  """
  @spec days_in_month(year, month) :: 28..31
  @impl true
  def days_in_month(year, month)

  def days_in_month(year, 2) do
    if leap_year?(year), do: 29, else: 28
  end

  def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
  def days_in_month(_, month) when month in 1..12, do: 31

  @doc """
  Returns if the given year is a leap year.

  ## Examples

      iex> Calendar.ISO.leap_year?(2000)
      true
      iex> Calendar.ISO.leap_year?(2001)
      false
      iex> Calendar.ISO.leap_year?(2004)
      true
      iex> Calendar.ISO.leap_year?(1900)
      false

  """
  @spec leap_year?(year) :: boolean()
  @impl true
  def leap_year?(year) when is_integer(year) and year >= 0 do
    rem(year, 4) === 0 and (rem(year, 100) > 0 or rem(year, 400) === 0)
  end

  @doc """
  Calculates the day of the week from the given `year`, `month`, and `day`.

  It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.

  ## Examples

      iex> Calendar.ISO.day_of_week(2016, 10, 31)
      1
      iex> Calendar.ISO.day_of_week(2016, 11, 01)
      2
      iex> Calendar.ISO.day_of_week(2016, 11, 02)
      3
      iex> Calendar.ISO.day_of_week(2016, 11, 03)
      4
      iex> Calendar.ISO.day_of_week(2016, 11, 04)
      5
      iex> Calendar.ISO.day_of_week(2016, 11, 05)
      6
      iex> Calendar.ISO.day_of_week(2016, 11, 06)
      7
  """
  @spec day_of_week(year, month, day) :: 1..7
  @impl true
  def day_of_week(year, month, day)
      when is_integer(year) and is_integer(month) and is_integer(day) do
    Integer.mod(date_to_iso_days(year, month, day) + 5, 7) + 1
  end

  @doc """
  Converts the given time into a string.

  ## Examples

      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 6})
      "02:02:02.000002"
      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 2})
      "02:02:02.00"
      iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 0})
      "02:02:02"
  """
  @spec time_to_string(
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: String.t()
  @impl true
  def time_to_string(hour, minute, second, microsecond) do
    time_to_string(hour, minute, second, microsecond, :extended)
  end

  def time_to_string(hour, minute, second, {_, 0}, format) do
    time_to_string_format(hour, minute, second, format)
  end

  def time_to_string(hour, minute, second, {microsecond, precision}, format) do
    time_to_string_format(hour, minute, second, format) <>
      "." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
  end

  defp time_to_string_format(hour, minute, second, :extended) do
    zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
  end

  defp time_to_string_format(hour, minute, second, :basic) do
    zero_pad(hour, 2) <> zero_pad(minute, 2) <> zero_pad(second, 2)
  end

  @doc """
  Converts the given date into a string.

  ## Examples

    iex> Calendar.ISO.date_to_string(2015, 2, 28)
    "2015-02-28"
    iex> Calendar.ISO.date_to_string(2017, 8, 1)
    "2017-08-01"
  """
  @spec date_to_string(year, month, day) :: String.t()
  @impl true
  def date_to_string(year, month, day) do
    date_to_string(year, month, day, :extended)
  end

  defp date_to_string(year, month, day, :extended) do
    zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
  end

  defp date_to_string(year, month, day, :basic) do
    zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
  end

  @doc """
  Converts the datetime (without time zone) into a string.

  ## Examples

    iex> Calendar.ISO.naive_datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 6})
    "2015-02-28 01:02:03.000004"
    iex> Calendar.ISO.naive_datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5})
    "2017-08-01 01:02:03.00000"
  """
  @impl true
  @spec naive_datetime_to_string(
          year,
          month,
          day,
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond()
        ) :: String.t()
  def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
    date_to_string(year, month, day) <> " " <> time_to_string(hour, minute, second, microsecond)
  end

  @doc """
  Converts the datetime (with time zone) into a string.

  ## Examples

    iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, "Europe/Berlin", "CET", 3600, 0)
    "2017-08-01 01:02:03.00000+01:00 CET Europe/Berlin"
    iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, "Europe/Berlin", "CDT", 3600, 3600)
    "2017-08-01 01:02:03.00000+02:00 CDT Europe/Berlin"
    iex> Calendar.ISO.datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 5}, "America/Los_Angeles", "PST", -28800, 0)
    "2015-02-28 01:02:03.00000-08:00 PST America/Los_Angeles"
    iex> Calendar.ISO.datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 5}, "America/Los_Angeles", "PDT", -28800, 3600)
    "2015-02-28 01:02:03.00000-07:00 PDT America/Los_Angeles"
  """
  @impl true
  @spec datetime_to_string(
          year,
          month,
          day,
          Calendar.hour(),
          Calendar.minute(),
          Calendar.second(),
          Calendar.microsecond(),
          Calendar.time_zone(),
          Calendar.zone_abbr(),
          Calendar.utc_offset(),
          Calendar.std_offset()
        ) :: String.t()
  def datetime_to_string(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        time_zone,
        zone_abbr,
        utc_offset,
        std_offset
      ) do
    date_to_string(year, month, day) <>
      " " <>
      time_to_string(hour, minute, second, microsecond) <>
      offset_to_string(utc_offset, std_offset, time_zone) <>
      zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
  end

  @doc """
  Determines if the date given is valid according to the proleptic Gregorian calendar.

  ## Examples

    iex(1)> Calendar.ISO.valid_date?(2015, 2, 28)
    true
    iex(2)> Calendar.ISO.valid_date?(2015, 2, 30)
    false
  """
  @impl true
  @since "1.5.0"
  @spec valid_date?(year, month, day) :: boolean
  def valid_date?(year, month, day) do
    month in 1..12 and year in 0..9999 and day in 1..days_in_month(year, month)
  end

  @doc """
  Determines if the date given is valid according to the proleptic Gregorian calendar.
  Note that leap seconds are considered valid, but the use of 24:00:00 as the
  zero hour of the day is considered invalid.

  ## Examples

    iex> Calendar.ISO.valid_time?(10, 50, 25, {3006, 6})
    true
    iex> Calendar.ISO.valid_time?(23, 59, 60, {0, 0})
    true
    iex> Calendar.ISO.valid_time?(24, 0, 0, {0, 0})
    false
  """
  @impl true
  @since "1.5.0"
  @spec valid_time?(Calendar.hour(), Calendar.minute(), Calendar.secon(), Calendar.microsecond()) ::
          boolean
  def valid_time?(hour, minute, second, {microsecond, precision}) do
    hour in 0..23 and minute in 0..59 and second in 0..60 and microsecond in 0..999_999 and
      precision in 0..6
  end

  @doc """
  See `c:Calendar.day_rollover_relative_to_midlight_utc/0` for documentation.
  """
  @impl true
  @since "1.5.0"
  @spec day_rollover_relative_to_midnight_utc() :: {0, 1}
  def day_rollover_relative_to_midnight_utc() do
    {0, 1}
  end

  defp offset_to_string(utc, std, zone, format \\ :extended)
  defp offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"

  defp offset_to_string(utc, std, _zone, format) do
    total = utc + std
    second = abs(total)
    minute = second |> rem(3600) |> div(60)
    hour = div(second, 3600)
    format_offset(total, hour, minute, format)
  end

  defp format_offset(total, hour, minute, :extended) do
    sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
  end

  defp format_offset(total, hour, minute, :basic) do
    sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
  end

  defp zone_to_string(0, 0, _abbr, "Etc/UTC"), do: ""
  defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone

  defp sign(total) when total < 0, do: "-"
  defp sign(_), do: "+"

  defp zero_pad(val, count) do
    num = Integer.to_string(val)
    :binary.copy("0", count - byte_size(num)) <> num
  end

  ## Helpers

  @doc false
  def from_unix(integer, unit) when is_integer(integer) do
    total = System.convert_time_unit(integer, unit, :microsecond)

    if total in @unix_range_microseconds do
      microseconds = Integer.mod(total, @microseconds_per_second)
      seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
      precision = precision_for_unit(unit)
      {date, time} = iso_seconds_to_datetime(seconds)
      {:ok, date, time, {microseconds, precision}}
    else
      {:error, :invalid_unix_time}
    end
  end

  defp precision_for_unit(unit) do
    subsecond = div(System.convert_time_unit(1, :second, unit), 10)
    precision_for_unit(subsecond, 0)
  end

  defp precision_for_unit(0, precision), do: precision
  defp precision_for_unit(_, 6), do: 6

  defp precision_for_unit(number, precision),
    do: precision_for_unit(div(number, 10), precision + 1)

  @doc false
  @since "1.5.0"
  def date_to_iso8601(year, month, day, format \\ :extended) do
    date_to_string(year, month, day, format)
  end

  @doc false
  def time_to_iso8601(hour, minute, second, microsecond, format \\ :extended) do
    time_to_string(hour, minute, second, microsecond, format)
  end

  @doc false
  def naive_datetime_to_iso8601(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        format \\ :extended
      ) do
    date_to_string(year, month, day, format) <>
      "T" <> time_to_string(hour, minute, second, microsecond, format)
  end

  @doc false
  def datetime_to_iso8601(
        year,
        month,
        day,
        hour,
        minute,
        second,
        microsecond,
        time_zone,
        _zone_abbr,
        utc_offset,
        std_offset,
        format \\ :extended
      ) do
    date_to_string(year, month, day, format) <>
      "T" <>
      time_to_string(hour, minute, second, microsecond, format) <>
      offset_to_string(utc_offset, std_offset, time_zone, format)
  end

  @doc false
  def parse_microsecond("." <> rest) do
    case parse_microsecond(rest, 0, "") do
      {"", 0, _} ->
        :error

      {microsecond, precision, rest} when precision in 1..6 ->
        pad = String.duplicate("0", 6 - byte_size(microsecond))
        {{String.to_integer(microsecond <> pad), precision}, rest}

      {microsecond, _precision, rest} ->
        {{String.to_integer(binary_part(microsecond, 0, 6)), 6}, rest}
    end
  end

  def parse_microsecond("," <> rest) do
    parse_microsecond("." <> rest)
  end

  def parse_microsecond(rest) do
    {{0, 0}, rest}
  end

  defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
    do: parse_microsecond(tail, precision + 1, <<acc::binary, head>>)

  defp parse_microsecond(rest, precision, acc), do: {acc, precision, rest}

  @doc false
  def parse_offset(""), do: {nil, ""}
  def parse_offset("Z"), do: {0, ""}
  def parse_offset("-00:00"), do: :error

  def parse_offset(<<?+, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
    do: parse_offset(1, hour, min, rest)

  def parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
    do: parse_offset(-1, hour, min, rest)

  def parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
    do: parse_offset(1, hour, min, rest)

  def parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
    do: parse_offset(-1, hour, min, rest)

  def parse_offset(<<?+, hour::2-bytes, rest::binary>>), do: parse_offset(1, hour, "00", rest)
  def parse_offset(<<?-, hour::2-bytes, rest::binary>>), do: parse_offset(-1, hour, "00", rest)
  def parse_offset(_), do: :error

  defp parse_offset(sign, hour, min, rest) do
    with {hour, ""} when hour < 24 <- Integer.parse(hour),
         {min, ""} when min < 60 <- Integer.parse(min) do
      {(hour * 60 + min) * 60 * sign, rest}
    else
      _ -> :error
    end
  end

  @doc false
  @since "1.5.0"
  def iso_days_to_unit({days, {parts, ppd}}, unit) do
    day_microseconds = days * @parts_per_day
    microseconds = div(parts * @parts_per_day, ppd)
    System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
  end

  @doc false
  @since "1.5.0"
  def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, ppd) do
    normalize_iso_days(days, parts + add, ppd)
  end

  def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, add_ppd) do
    parts = parts * add_ppd
    add = add * ppd
    gcd = Integer.gcd(ppd, add_ppd)
    result_parts = div(parts + add, gcd)
    result_ppd = div(ppd * add_ppd, gcd)
    normalize_iso_days(days, result_parts, result_ppd)
  end

  defp normalize_iso_days(days, parts, ppd) do
    days_offset = div(parts, ppd)
    parts = rem(parts, ppd)

    if parts < 0 do
      {days + days_offset - 1, {parts + ppd, ppd}}
    else
      {days + days_offset, {parts, ppd}}
    end
  end

  # Note that this function does not add the extra leap day for a leap year.
  # If you want to add that leap day when appropriate,
  # add the result of leap_day_offset/2 to the result of days_before_month/1.
  defp days_before_month(1), do: 0
  defp days_before_month(2), do: 31
  defp days_before_month(3), do: 59
  defp days_before_month(4), do: 90
  defp days_before_month(5), do: 120
  defp days_before_month(6), do: 151
  defp days_before_month(7), do: 181
  defp days_before_month(8), do: 212
  defp days_before_month(9), do: 243
  defp days_before_month(10), do: 273
  defp days_before_month(11), do: 304
  defp days_before_month(12), do: 334

  defp leap_day_offset(_year, month) when month < 3, do: 0

  defp leap_day_offset(year, _month) do
    if leap_year?(year), do: 1, else: 0
  end

  defp days_to_year(days) do
    year = Integer.floor_div(days, @days_per_nonleap_year)
    {year, days_before_year} = days_to_year(year, days, days_in_previous_years(year))
    {year, days - days_before_year}
  end

  defp days_to_year(year, days1, days2) when days1 < days2 do
    days_to_year(year - 1, days1, days_in_previous_years(year - 1))
  end

  defp days_to_year(year, _days1, days2) do
    {year, days2}
  end

  defp days_in_previous_years(0), do: 0

  defp days_in_previous_years(year) do
    previous_year = year - 1

    Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
      Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
      @days_per_leap_year
  end

  # Note that 0 is the first day of the month.
  defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
    {1, day_of_year}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
    {2, day_of_year - 31}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
    {3, day_of_year - (59 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
    {4, day_of_year - (90 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
    {5, day_of_year - (120 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
    {6, day_of_year - (151 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
    {7, day_of_year - (181 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
    {8, day_of_year - (212 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
    {9, day_of_year - (243 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
    {10, day_of_year - (273 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
    {11, day_of_year - (304 + extra_day)}
  end

  defp year_day_to_year_date(extra_day, day_of_year) do
    {12, day_of_year - (334 + extra_day)}
  end

  defp iso_seconds_to_datetime(seconds) do
    {days, rest_seconds} = div_mod(seconds, @seconds_per_day)

    date = date_from_iso_days(days)
    time = seconds_to_time(rest_seconds)
    {date, time}
  end

  defp seconds_to_time(seconds) when seconds in 0..(@seconds_per_day - 1) do
    {hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
    {minute, second} = div_mod(rest_seconds, @seconds_per_minute)

    {hour, minute, second}
  end
end


get rid of pipe for single func call

get rid of pipe for single func call

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator
  alias ExTwilio.Config

  @type url :: String.t()

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(fn -> {url, module, options} end, &process_page/1, fn _ -> nil end)
  end

  @spec fetch_page(url, module, options :: list) :: {list, {url, module, options :: list}}
  defp fetch_page(url, module, options) do
    results = Api.get!(url, Api.auth_header(options))
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, {meta["next_page_uri"], module, options}}
  end

  @spec process_page({url | nil, module, options :: list}) ::
          {:halt, nil}
          | {list, {url, module}}
  defp process_page({nil, _module, _options}), do: {:halt, nil}

  defp process_page({next_page_uri, module, options}) do
    fetch_page(next_page_uri, module, options)
  end
end

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator
  alias ExTwilio.Config

  @type url :: String.t()

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(fn -> {url, module, options} end, &process_page/1, fn _ -> nil end)
  end

  @spec fetch_page(url, module, options :: list) :: {list, {url, module, options :: list}}
  defp fetch_page(url, module, options) do
    results = Api.get!(url, Api.auth_header(options))
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, {meta["next_page_uri"], module, options}}
  end

  @spec process_page({url | nil, module, options :: list}) ::
          {:halt, nil}
          | {list, {url, module}}
  defp process_page({nil, _module, _options}), do: {:halt, nil}

  defp process_page({next_page_uri, module, options}) do
    next_page_uri
    |> fetch_page(module, options)
  end
end


Improve docs

Improve docs

defmodule Credo.Check.Readability.SpaceAfterCommas do
  @moduledoc """
  You can use white-space after commas to make items of lists,
  tuples and other enumerations easier to separate from one another.

      # preferred

      alias Project.{Alpha, Beta}

      def some_func(first, second, third) do
        list = [1, 2, 3, 4, 5]
        # ...
      end

      # NOT preferred - items are harder to separate

      alias Project.{Alpha,Beta}

      def some_func(first,second,third) do
        list = [1,2,3,4,5]
        # ...
      end

  Like all `Readability` issues, this one is not a technical concern.
  But you can improve the odds of others reading and liking your code by making
  it easier to follow.
  """

  @explanation [check: @moduledoc]

  # Matches commas followed by non-whitespace unless preceded by
  # a question mark that is not part of a variable or function name
  @unspaced_commas ~r/(?<!\W\?)(\,\S)/

  use Credo.Check
  alias Credo.Check.CodeHelper
  alias Credo.Code

  @doc false
  def run(source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    source_file
    |> CodeHelper.clean_charlists_strings_sigils_and_comments
    |> Code.to_lines
    |> Enum.flat_map(&find_issues(issue_meta, &1))
  end

  defp issue_for(issue_meta, trigger, line_no, column) do
    format_issue issue_meta,
      message: "Space missing after comma",
      trigger: trigger,
      line_no: line_no,
      column: column
  end

  defp find_issues(issue_meta, {line_no, line}) do
    @unspaced_commas
    |> Regex.scan(line, capture: :all_but_first, return: :index)
    |> List.flatten
    |> Enum.map(fn({idx, len}) ->
      trigger = String.slice(line, idx, len)
      issue_for(issue_meta, trigger, line_no, idx + 1)
    end)
  end
end

defmodule Credo.Check.Readability.SpaceAfterCommas do
  @moduledoc """
  You can use white-space after commas to make items of lists,
  tuples and other enumerations easier to separate from one another.

      # preferred

      alias Project.{Alpha, Beta}
      arr = [1, 2, 3, 4, 5]
      def some_func(first, second, third) do
        ...
      end

      # NOT preferred - items are harder to separate

      alias Project.{Alpha,Beta}
      arr = [1,2,3,4,5]
      def some_func(first,second,third) do
        ...
      end

  Like all `Readability` issues, this one is not a technical concern.
  But you can improve the odds of others reading and liking your code by making
  it easier to follow.
  """

  @explanation [check: @moduledoc]

  # Matches commas followed by non-whitespace unless preceded by
  # a question mark that is not part of a variable or function name
  @unspaced_commas ~r/(?<!\W\?)(\,\S)/

  use Credo.Check
  alias Credo.Check.CodeHelper
  alias Credo.Code

  @doc false
  def run(source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    source_file
    |> CodeHelper.clean_charlists_strings_sigils_and_comments
    |> Code.to_lines
    |> Enum.flat_map(&find_issues(issue_meta, &1))
  end

  defp issue_for(issue_meta, trigger, line_no, column) do
    format_issue issue_meta,
      message: "Space missing after comma",
      trigger: trigger,
      line_no: line_no,
      column: column
  end

  defp find_issues(issue_meta, {line_no, line}) do
    @unspaced_commas
    |> Regex.scan(line, capture: :all_but_first, return: :index)
    |> List.flatten
    |> Enum.map(fn({idx, len}) ->
      trigger = String.slice(line, idx, len)
      issue_for(issue_meta, trigger, line_no, idx + 1)
    end)
  end
end


back to caching thumbnails

back to caching thumbnails

defmodule PanWeb.MaintenanceController do
  use Pan.Web, :controller
  alias PanWeb.{Episode, Persona, Podcast}

  def vienna_beamers(conn, _params) do
    redirect(conn, external: "https://blog.panoptikum.io/vienna-beamers/")
  end

  def blog_2016(conn, %{"month" => month, "day" => day, "file" => file}) do
    redirect(conn, external: "https://blog.panoptikum.io/2016/#{month}/#{day}/#{file}")
  end

  def blog_2017(conn, %{"month" => month, "day" => day, "file" => file}) do
    redirect(conn, external: "https://blog.panoptikum.io/2017/#{month}/#{day}/#{file}")
  end

  def fix(conn, _params) do
    Persona.cache_missing_thumbnail_images()

    render(conn, "done.html")
  end

  def update_podcast_counters(conn, _params) do
    Podcast.update_all_counters()

    render(conn, "done.html")
  end

  def sandbox(conn, _params) do
    render(conn, "sandbox.html")
  end

  def stats(conn, _params) do
    stale_podcasts =
      from(p in Podcast, where: p.next_update <= ^Timex.now() and
                                (is_nil(p.update_paused) or p.update_paused == false) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:count, :id)
      |> delimit_integer(" ")

    inactive_podcasts =
      from(p in Podcast, where: (p.update_paused == true) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:count, :id)

    retired_podcasts =
      from(p in Podcast, where: p.retired == true)
      |> Repo.aggregate(:count, :id)

    average_update_intervall =
      from(p in Podcast, where: (is_nil(p.update_paused) or p.update_paused == false) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:avg, :update_intervall)
      |> Decimal.round(2)

    total_podcasts = Repo.aggregate(Podcast, :count, :id)
                     |> delimit_integer(" ")

    total_episodes = Repo.aggregate(PanWeb.Podcast, :sum, :episodes_count)
                     |> delimit_integer(" ")

    unindexed_episodes =
      from(p in Episode, where: (is_nil(p.elastic) or p.elastic == false))
      |> Repo.aggregate(:count, :id)
      |> delimit_integer(" ")

    podcasts_per_hour = Repo.aggregate(Podcast, :count, :id) - inactive_podcasts
                        |> Decimal.new()
                        |> Decimal.div(average_update_intervall)
                        |> Decimal.round()

    total_users = Repo.aggregate(PanWeb.User, :count, :id)
    total_gigs = Repo.aggregate(PanWeb.Gig, :count, :id)
                 |> delimit_integer(" ")
    total_engagements = Repo.aggregate(PanWeb.Engagement, :count, :id)
                        |> delimit_integer(" ")
    total_personas = Repo.aggregate(PanWeb.Persona, :count, :id)
                     |> delimit_integer(" ")
    total_subscriptions = Repo.aggregate(PanWeb.Subscription, :count, :id)
                          |> delimit_integer(" ")
    total_likes = Repo.aggregate(PanWeb.Like, :count, :id)
    total_recommendations = Repo.aggregate(PanWeb.Recommendation, :count, :id)
    total_categories = Repo.aggregate(PanWeb.Category, :count, :id)
    total_categorizations = Repo.aggregate("categories_podcasts", :count, :podcast_id)
                            |> delimit_integer(" ")
    total_languages = Repo.aggregate(PanWeb.Language, :count, :id)
    total_opmls = Repo.aggregate(PanWeb.Opml, :count, :id)
    total_feed_backlogs = Repo.aggregate(PanWeb.FeedBacklog, :count, :id)
    total_follows = Repo.aggregate(PanWeb.Follow, :count, :id)
    total_manifestations = Repo.aggregate(PanWeb.Manifestation, :count, :id)
    total_delegations = Repo.aggregate(PanWeb.Delegation, :count, :id)

    render(conn, "stats.html", stale_podcasts: stale_podcasts,
                               inactive_podcasts: inactive_podcasts,
                               retired_podcasts: retired_podcasts,
                               average_update_intervall: average_update_intervall,
                               total_podcasts: total_podcasts,
                               total_episodes: total_episodes,
                               podcasts_per_hour: podcasts_per_hour,
                               total_users: total_users,
                               total_gigs: total_gigs,
                               total_engagements: total_engagements,
                               total_personas: total_personas,
                               total_subscriptions: total_subscriptions,
                               total_likes: total_likes,
                               total_recommendations: total_recommendations,
                               total_categories: total_categories,
                               total_categorizations: total_categorizations,
                               total_languages: total_languages,
                               total_opmls: total_opmls,
                               total_feed_backlogs: total_feed_backlogs,
                               total_follows: total_follows,
                               total_manifestations: total_manifestations,
                               total_delegations: total_delegations,
                               unindexed_episodes: unindexed_episodes)

  end

  defp delimit_integer(number, delimiter) do
    abs(number)
    |> Integer.to_charlist
    |> :lists.reverse
    |> delimit_integer(delimiter, [])
  end
  defp delimit_integer([a,b,c,d|tail], delimiter, acc) do
    delimit_integer([d|tail], delimiter, [delimiter,c,b,a|acc])
  end
  defp delimit_integer(list, _, acc) do
    :lists.reverse(list) ++ acc
  end
end

defmodule PanWeb.MaintenanceController do
  use Pan.Web, :controller
  alias PanWeb.{Episode, Persona, Podcast}

  def vienna_beamers(conn, _params) do
    redirect(conn, external: "https://blog.panoptikum.io/vienna-beamers/")
  end

  def blog_2016(conn, %{"month" => month, "day" => day, "file" => file}) do
    redirect(conn, external: "https://blog.panoptikum.io/2016/#{month}/#{day}/#{file}")
  end

  def blog_2017(conn, %{"month" => month, "day" => day, "file" => file}) do
    redirect(conn, external: "https://blog.panoptikum.io/2017/#{month}/#{day}/#{file}")
  end

  def fix(conn, _params) do
    gigs = from(g in PanWeb.Gig, where: is_nil(g.publishing_date))
           |> Repo.all()

    for gig <- gigs do
      gig
      |> PanWeb.Gig.changeset(%{publishing_date: gig.inserted_at})
      |> Repo.update()
    end

    episodes = from(e in PanWeb.Episode, where: is_nil(e.publishing_date))
           |> Repo.all()

    for episode <- episodes do
      episode
      |> PanWeb.Episode.changeset(%{publishing_date: episode.inserted_at,
                                    link: episode.link || "http://example.com"})
      |> Repo.update()
    end

    # Persona.cache_missing_thumbnail_images()

    render(conn, "done.html")
  end

  def update_podcast_counters(conn, _params) do
    Podcast.update_all_counters()

    render(conn, "done.html")
  end

  def sandbox(conn, _params) do
    render(conn, "sandbox.html")
  end

  def stats(conn, _params) do
    stale_podcasts =
      from(p in Podcast, where: p.next_update <= ^Timex.now() and
                                (is_nil(p.update_paused) or p.update_paused == false) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:count, :id)
      |> delimit_integer(" ")

    inactive_podcasts =
      from(p in Podcast, where: (p.update_paused == true) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:count, :id)

    retired_podcasts =
      from(p in Podcast, where: p.retired == true)
      |> Repo.aggregate(:count, :id)

    average_update_intervall =
      from(p in Podcast, where: (is_nil(p.update_paused) or p.update_paused == false) and
                                (is_nil(p.retired) or p.retired == false))
      |> Repo.aggregate(:avg, :update_intervall)
      |> Decimal.round(2)

    total_podcasts = Repo.aggregate(Podcast, :count, :id)
                     |> delimit_integer(" ")

    total_episodes = Repo.aggregate(PanWeb.Podcast, :sum, :episodes_count)
                     |> delimit_integer(" ")

    unindexed_episodes =
      from(p in Episode, where: (is_nil(p.elastic) or p.elastic == false))
      |> Repo.aggregate(:count, :id)
      |> delimit_integer(" ")

    podcasts_per_hour = Repo.aggregate(Podcast, :count, :id) - inactive_podcasts
                        |> Decimal.new()
                        |> Decimal.div(average_update_intervall)
                        |> Decimal.round()

    total_users = Repo.aggregate(PanWeb.User, :count, :id)
    total_gigs = Repo.aggregate(PanWeb.Gig, :count, :id)
                 |> delimit_integer(" ")
    total_engagements = Repo.aggregate(PanWeb.Engagement, :count, :id)
                        |> delimit_integer(" ")
    total_personas = Repo.aggregate(PanWeb.Persona, :count, :id)
                     |> delimit_integer(" ")
    total_subscriptions = Repo.aggregate(PanWeb.Subscription, :count, :id)
                          |> delimit_integer(" ")
    total_likes = Repo.aggregate(PanWeb.Like, :count, :id)
    total_recommendations = Repo.aggregate(PanWeb.Recommendation, :count, :id)
    total_categories = Repo.aggregate(PanWeb.Category, :count, :id)
    total_categorizations = Repo.aggregate("categories_podcasts", :count, :podcast_id)
                            |> delimit_integer(" ")
    total_languages = Repo.aggregate(PanWeb.Language, :count, :id)
    total_opmls = Repo.aggregate(PanWeb.Opml, :count, :id)
    total_feed_backlogs = Repo.aggregate(PanWeb.FeedBacklog, :count, :id)
    total_follows = Repo.aggregate(PanWeb.Follow, :count, :id)
    total_manifestations = Repo.aggregate(PanWeb.Manifestation, :count, :id)
    total_delegations = Repo.aggregate(PanWeb.Delegation, :count, :id)

    render(conn, "stats.html", stale_podcasts: stale_podcasts,
                               inactive_podcasts: inactive_podcasts,
                               retired_podcasts: retired_podcasts,
                               average_update_intervall: average_update_intervall,
                               total_podcasts: total_podcasts,
                               total_episodes: total_episodes,
                               podcasts_per_hour: podcasts_per_hour,
                               total_users: total_users,
                               total_gigs: total_gigs,
                               total_engagements: total_engagements,
                               total_personas: total_personas,
                               total_subscriptions: total_subscriptions,
                               total_likes: total_likes,
                               total_recommendations: total_recommendations,
                               total_categories: total_categories,
                               total_categorizations: total_categorizations,
                               total_languages: total_languages,
                               total_opmls: total_opmls,
                               total_feed_backlogs: total_feed_backlogs,
                               total_follows: total_follows,
                               total_manifestations: total_manifestations,
                               total_delegations: total_delegations,
                               unindexed_episodes: unindexed_episodes)

  end

  defp delimit_integer(number, delimiter) do
    abs(number)
    |> Integer.to_charlist
    |> :lists.reverse
    |> delimit_integer(delimiter, [])
  end
  defp delimit_integer([a,b,c,d|tail], delimiter, acc) do
    delimit_integer([d|tail], delimiter, [delimiter,c,b,a|acc])
  end
  defp delimit_integer(list, _, acc) do
    :lists.reverse(list) ++ acc
  end
end


remove hard-deprecated Mix.Config for elixir 1.13


Add the date to Logger lines and force UTC timestamps

This is otherwise a copy of the default format.

use Mix.Config

config :logger,
  utc_log: true

config :logger, :console,
  format: "$date $time $metadata[$level] $levelpad$message\n"

# 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 :gullintanni, key: :value
#
# And access this configuration in your application as:
#
#     Application.get_env(:gullintanni, :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"


Add one more test for LargeNumbers

Add one more test for LargeNumbers

defmodule Credo.Check.Readability.LargeNumbersTest do
  use Credo.TestHelper

  @described_check Credo.Check.Readability.LargeNumbers

  @tag needs_elixir: "1.3.2"
  test "it should NOT report expected code" do
"""
@budgets %{
  "budget1": 100_000,
  "budget2": 200_000,
  "budget3": 300_000,
  "budget4": 500_000,
  "budget5": 1_000_000,
  "budget6": 2_000_000
}

@int32_min -2_147_483_648
@int32_max  2_147_483_647
@int64_min -9_223_372_036_854_775_808
@int64_max  9_223_372_036_854_775_807

def numbers do
  1024 + 1_000_000 + 11_000 + 22_000 + 33_000
  10_000..20_000
end
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end

  test "it should report a violation" do
"""
def numbers do
  1024 + 1000000 + 43534
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report a violation, since it is formatted incorrectly" do
"""
def numbers do
  1024 + 10_00_00_0 + 43534
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report only one violation" do
"""
def numbers do
  1024 + 1000000 + 43534
end
""" |> to_source_file
    |> assert_issue(@described_check, only_greater_than: 50000)
  end

  test "it should report only one violation for ranges /1" do
"""
def numbers do
  10000..20_000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report only one violation for ranges /2" do
"""
def numbers do
  10_000..20000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report only two violation for ranges" do
"""
def numbers do
  10000..20000
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report a violation /2" do
"""
defp numbers do
  1024 + 43534
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report a violation /3" do
"""
defp numbers do
  1024 + 43534.0
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report a violation /4" do
"""
defmacro numbers do
  1024 + 1_000000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should format floating point numbers nicely" do
"""
def numbers do
  10000.00001
end
""" |> to_source_file
    |> assert_issue(@described_check, fn(%Credo.Issue{message: message}) ->
      assert Regex.run(~r/[\d\._]+/, message) |> hd ==  "10_000.00001"
    end)
  end

  test "it should report all digits from the source" do
"""
def numbers do
  10000.000010
end
""" |> to_source_file
    |> assert_issue(@described_check, fn(%Credo.Issue{message: message}) ->
      assert Regex.run(~r/[\d\._]+/, message) |> hd ==  "10_000.000010"
    end)
  end

  test "it should not complain about non-decimal numbers" do
"""
def numbers do
  0xFFFF
  0b1111_1111_1111_1111
  0o777_777
end
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end

  test "check old false positive is fixed /1" do
    " defmacro oid_ansi_x9_62, do: quote do: {1,2,840,10_045}"
    |> to_source_file
    |> refute_issues(@described_check)
  end

  @tag needs_elixir: "1.3.2"
  test "check old false positive is fixed /2" do
"""
%{
  bounds: [
    0, 1, 2, 5, 10, 20, 30, 65, 85,
    100, 200, 400, 800,
    1_000, 2_000, 4_000, 8_000, 16_000]
}
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end
end

defmodule Credo.Check.Readability.LargeNumbersTest do
  use Credo.TestHelper

  @described_check Credo.Check.Readability.LargeNumbers

  @tag needs_elixir: "1.3.2"
  test "it should NOT report expected code" do
"""
@budgets %{
  "budget1": 100_000,
  "budget2": 200_000,
  "budget3": 300_000,
  "budget4": 500_000,
  "budget5": 1_000_000,
  "budget6": 2_000_000
}

def numbers do
  1024 + 1_000_000 + 11_000 + 22_000 + 33_000
  10_000..20_000
end
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end

  test "it should report a violation" do
"""
def numbers do
  1024 + 1000000 + 43534
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report a violation, since it is formatted incorrectly" do
"""
def numbers do
  1024 + 10_00_00_0 + 43534
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report only one violation" do
"""
def numbers do
  1024 + 1000000 + 43534
end
""" |> to_source_file
    |> assert_issue(@described_check, only_greater_than: 50000)
  end

  test "it should report only one violation for ranges /1" do
"""
def numbers do
  10000..20_000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report only one violation for ranges /2" do
"""
def numbers do
  10_000..20000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report only two violation for ranges" do
"""
def numbers do
  10000..20000
end
""" |> to_source_file
    |> assert_issues(@described_check)
  end

  test "it should report a violation /2" do
"""
defp numbers do
  1024 + 43534
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report a violation /3" do
"""
defp numbers do
  1024 + 43534.0
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should report a violation /4" do
"""
defmacro numbers do
  1024 + 1_000000
end
""" |> to_source_file
    |> assert_issue(@described_check)
  end

  test "it should format floating point numbers nicely" do
"""
def numbers do
  10000.00001
end
""" |> to_source_file
    |> assert_issue(@described_check, fn(%Credo.Issue{message: message}) ->
      assert Regex.run(~r/[\d\._]+/, message) |> hd ==  "10_000.00001"
    end)
  end

  test "it should report all digits from the source" do
"""
def numbers do
  10000.000010
end
""" |> to_source_file
    |> assert_issue(@described_check, fn(%Credo.Issue{message: message}) ->
      assert Regex.run(~r/[\d\._]+/, message) |> hd ==  "10_000.000010"
    end)
  end

  test "it should not complain about non-decimal numbers" do
"""
def numbers do
  0xFFFF
  0b1111_1111_1111_1111
  0o777_777
end
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end

  test "check old false positive is fixed /1" do
    " defmacro oid_ansi_x9_62, do: quote do: {1,2,840,10_045}"
    |> to_source_file
    |> refute_issues(@described_check)
  end

  @tag needs_elixir: "1.3.2"
  test "check old false positive is fixed /2" do
"""
%{
  bounds: [
    0, 1, 2, 5, 10, 20, 30, 65, 85,
    100, 200, 400, 800,
    1_000, 2_000, 4_000, 8_000, 16_000]
}
"""
    |> to_source_file
    |> refute_issues(@described_check)
  end
end


Mark Ben Marx as a board member (#395)

Mark Ben Marx as a board member (#395)

And remove Richard Carlsson from the role now that the votes have gone through and the board has approved the elections.

vols = [
  %{
    avatar_url: "volunteers/tristan-sloughter.jpg",
    id: "4a9698ea-6bb2-4331-8846-0248246ab346",
    member_id: "e666dc26-9085-4f8d-a4c2-8bbada4905e7",
    name: "Tristan Sloughter"
  },
  %{
    avatar_url: "volunteers/fred-hebert.png",
    id: "8d8d7d05-cc79-445a-8e37-c56525024f7e",
    member_id: "69be5f73-fcae-4844-adbb-7de0d2d613b3",
    name: "Fred Hebert",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/bryan-paxton.jpg",
    id: "2641e9f6-6f2d-497d-b0ab-4f9f4d033115",
    member_id: "37370312-7e08-4a7f-b921-7bb9daafa956",
    name: "Bryan Paxton"
  },
  %{
    avatar_url: "volunteers/todd-resudek.jpg",
    id: "3d208e24-68dd-4d05-b2c9-e1fcbf8bb6d9",
    member_id: "1bb949e0-649d-4a41-883b-dceed1fa1d99",
    name: "Todd Resudek"
  },
  %{
    avatar_url: "volunteers/randy-thompson.jpg",
    id: "2565fbd1-dc33-4258-93a0-358f1b0a2cfd",
    member_id: "fe65ceeb-dc26-4daa-a4f3-62654379b556",
    name: "Randy Thompson"
  },
  %{
    avatar_url: "volunteers/wojtek-mach.jpg",
    id: "72491fc9-8c24-45a9-8119-2cd12fdae830",
    member_id: "71efaa52-a8ad-4bcc-af36-52fb20fe37c7",
    name: "Wojtek Mach"
  },
  %{
    avatar_url: nil,
    id: "e2d3bf92-50ae-422f-bcda-11aa62cebc47",
    member_id: "236d2723-1bdb-4711-8c1c-daa166d724fb",
    name: "Ivan Glushkov"
  },
  %{
    avatar_url: "volunteers/andrea-leopardi.jpg",
    id: "faab4673-f41b-4784-81c2-39eaa1b3d658",
    member_id: "8b79dadc-0c4c-4ed0-90b6-8967cb743249",
    name: "Andrea Leopardi"
  },
  %{
    avatar_url: nil,
    id: "29475e68-a75d-479d-8f3d-c35d2ade468b",
    member_id: "1a17a6c0-acce-4b96-b6de-a058adf8bc1d",
    name: "Justin Wood"
  },
  %{
    avatar_url: "volunteers/jose-valim.jpg",
    id: "affdba10-18a8-4402-9847-fe1976166dc3",
    member_id: "84e26a9b-ddf5-4690-97ee-742afb48a689",
    name: "José Valim",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "0abe96bb-690c-4b27-b978-ff8d8772321a",
    member_id: "9fa2e441-80fe-40c8-9836-a42030ed6d7d",
    name: "Thomas Depierre"
  },
  %{
    avatar_url: "volunteers/adam-lindberg.jpg",
    id: "8f22ac12-badc-4a71-80fa-4de70051ee2b",
    member_id: "feea4a07-2128-40a1-ad0a-df3a7a79bdb1",
    name: "Adam Lindberg"
  },
  %{
    avatar_url: "volunteers/maxim-fedorov.jpg",
    id: "4060d400-7b66-4724-ad16-2f813a94c8f7",
    member_id: "e9d55e5a-8337-4f31-8224-ff343013072d",
    name: "Maxim Fedorov",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "8a05166f-b2ec-466c-9f49-a4b148947d4a",
    member_id: "9ca23638-5855-4234-88f3-e0cdb5da849f",
    name: "John Hogberg"
  },
  %{
    avatar_url: "volunteers/kenneth-lundin.jpg",
    id: "9c129b15-b008-4947-b98b-def281e4535d",
    member_id: "a1fb3069-3597-4427-9ed8-c48f7624b096",
    name: "Kenneth Lundin",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/mariano-guerra.jpg",
    id: "6abaa50e-7b7a-4a1f-afcf-c2ad999c97ab",
    member_id: "62f2a89e-c773-47d5-b9e0-ca3beaea4507",
    name: "Mariano Guerra"
  },
  %{
    avatar_url: "volunteers/radek-szymczyszyn.jpg",
    id: "8fc7330f-72ef-4aba-a57f-2a7658453ad7",
    member_id: "ff80f5fa-bf24-4381-9629-f5fa0727bcdb",
    name: "Radosław Szymczyszyn"
  },
  %{
    avatar_url: "volunteers/laszlo-bacsi.png",
    id: "aa8e84d5-a63a-48bd-a5a3-c249bd8f99f1",
    member_id: "b24a9d17-fe71-4306-bf10-156f95475517",
    name: "László Bácsi"
  },
  %{
    avatar_url: "volunteers/brujo-benavides.png",
    id: "bdbd55e3-e8ab-41ee-9fac-746af27e7f92",
    member_id: "1a6b1515-4397-46fb-b4cb-361a7171893e",
    name: "Brujo Benavides"
  },
  %{
    avatar_url: "volunteers/francesco-cesarini.jpg",
    id: "589b130f-0ae5-410c-b1d1-3fcaf55d0f6f",
    member_id: "a5c4efd3-e235-4bf4-85f5-57b18a1a0446",
    name: "Francesco Cesarini",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/miriam-pena.jpg",
    id: "305cf09b-7cc0-4a29-b02c-d47301a762fb",
    member_id: "9cb09897-e979-4263-82e6-310b78429dc4",
    name: "Miriam Pena",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/peer-stritzinger.jpg",
    id: "a3fe00bc-e734-4f84-8e72-92c5fef519e9",
    member_id: "8e905dc9-e7d1-45c4-9844-68a5225fb474",
    name: "Peer Stritzinger",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/simon-thompson.jpg",
    id: "b28e5e4a-1480-49c5-90c8-5406654c7095",
    member_id: "5d5a10a2-4922-48e4-8bd8-1f2a682180e5",
    name: "Simon Thompson"
  },
  %{
    avatar_url: "volunteers/jeff-grunewald.jpg",
    id: "aed2f5f9-cf21-4a9f-9302-a732c980e038",
    member_id: "b71b30ab-b6d0-4ccb-b6cd-fa24f3964972",
    name: "Jeff Grunewald"
  },
  %{
    avatar_url: "volunteers//anna-neyzberg.jpg",
    id: "f9e4d9a9-d8c1-4f99-9008-1a4a82d8821b",
    member_id: "758f211d-4558-4430-9350-ba65025cc86e",
    name: "Anna Neyzberg"
  },
  %{
    avatar_url: "volunteers/laura-castro.jpg",
    id: "62fb737d-239d-4139-817c-1c907fabb273",
    member_id: "0c0258a2-cc67-4a4a-b86b-f36b75e7c21a",
    name: "Laura M. Castro"
  },
  %{
    avatar_url: "volunteers/mrinal-wadhwa.png",
    id: "9af5a1f6-2f1f-4210-8d66-4743c5f4489b",
    member_id: "ff6606c1-631d-48a2-934c-2ea01a900fec",
    name: "Mrinal Wadhwa"
  },
  %{
    avatar_url: "volunteers/ben-marx.jpg",
    id: "2cf7ff99-6d98-4a54-8dc6-d63c0797af0a",
    member_id: "cf74b2b1-c51f-42d0-b6ec-425954c75cd5",
    name: "Ben Marx",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/alistair-woodman.png",
    id: "c8eb633a-4acd-4f8d-8ff2-2a25892e439b",
    member_id: "8b16bbfa-eeb4-4897-94a7-6a8da94ee522",
    name: "Alistair Woodman",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/paulo-d-gonzalez.png",
    id: "dff72785-a7f3-4a12-9e10-3475924d3969",
    member_id: "89f4873e-8d55-4154-af28-0c68c53512e4",
    name: "Paulo D. Gonzalez"
  },
  %{
    avatar_url: "volunteers/norberto-ortigoza.jpg",
    id: "9dbcf746-740d-48fa-85de-52d28b8e0f5e",
    member_id: "8e1c0df6-662d-4030-ae4e-8ad9f189dd5d",
    name: "Norberto Ortigoza"
  },
  %{
    avatar_url: "volunteers/adolfo-neto.jpg",
    id: "cbb6ce77-6d02-4ab2-9211-3d59e363b557",
    member_id: "8252209a-36dc-478b-9e81-eb103ee4ecc1",
    name: "Adolfo Neto"
  },
  %{
    avatar_url: "volunteers/johnny-winn.jpeg",
    id: "4bb5466e-440e-4424-b692-2e8fc42fb039",
    member_id: "8ec02dcd-56ee-42cb-832a-5bdc7cae3c59",
    name: "Johnny Winn"
  },
  %{
    avatar_url: "volunteers/raphael-vidal.jpeg",
    id: "352ea96b-6420-4351-827b-bfb37ea0634b",
    member_id: "a7c7e112-1727-4cc0-8806-ec7cff41718b",
    name: "Raphael Vidal"
  },
  %{
    avatar_url: "volunteers/annette-bieniusa.jpeg",
    id: "481982ea-5a7e-46ea-92a3-e0fdc95a9181",
    member_id: "848018bf-9ddf-4d29-8eca-171fd527ed58",
    name: "Annette Bieniusa"
  },
  %{
    avatar_url: "volunteers/bruno-azenha.jpeg",
    id: "0f28fdaa-4860-4f83-8c63-169982276a11",
    member_id: "8b01db41-3603-4277-9623-cb4ec1fde90b",
    name: "Bruno Azenha Gonçalves"
  },
  %{
    avatar_url: "volunteers/eric-douglas.jpeg",
    id: "d8ea3abc-d647-4475-b5dc-c1fbea1378d8",
    member_id: "d290e600-4e5d-4065-aeda-969fe4c10805",
    name: "Eric Douglas"
  },
  %{
    avatar_url: nil,
    id: "852e7313-0fff-40fa-a4a3-f505aadcd4df",
    member_id: "34c27e55-6b34-4dfa-ab43-7efb46a9ec22",
    name: "Frank Hunleth"
  },
  %{
    avatar_url: "volunteers/justin-schneck.jpg",
    id: "ee9779a3-e48f-4bf5-b47d-f60321ff19aa",
    member_id: "985ae4fa-0c74-4991-ab23-7ea4b7b3d3db",
    name: "Justin Schneck"
  },
  %{
    avatar_url: "volunteers/hideki-takase.jpg",
    id: "055cbfc1-156b-4c48-b274-696176204b49",
    member_id: "3e1f0557-c5ee-453d-b96c-f052016f7b05",
    name: "Hideki Takase"
  },
  %{
    avatar_url: "volunteers/natalia_chechina.jpg",
    id: "68adf5c8-9564-490b-b364-cfe145ac38a6",
    member_id: "e0c2bc7a-5a6d-4ba0-a67a-1928af53e3e7",
    name: "Natalia Chechina"
  },
  %{
    avatar_url: "volunteers/susumu-yamazaki.jpg",
    id: "dc5c56fe-52ab-479f-97ed-4fc10c3a5d35",
    member_id: "febcf842-ebf5-4919-a2b0-a958c45f6ab2",
    name: "Susumu Yamazaki"
  },
  %{
    avatar_url: "volunteers/akash-hiremath.jpg",
    id: "2c95b096-91db-4da9-8633-dc118e5b434a",
    member_id: "a4557964-2a79-4f48-91ae-7a882a0905db",
    name: "Akash Hiremath"
  },
  %{
    avatar_url: "volunteers/graham-leva.jpg",
    id: "5f84e9dd-26b7-4f48-a4dc-6690f3ff6df2",
    member_id: "1b71f264-cf2d-442d-9a49-5ad238dc9a3d",
    name: "Graham Leva"
  },
  %{
    avatar_url: "volunteers/isaac-yonemoto.jpg",
    id: "8ecfd6ab-4c5f-42bc-adbf-9d5d54d59957",
    member_id: "6b687258-6418-4c07-bac1-34976c34476e",
    name: "Isaac Yonemoto"
  },
  %{
    avatar_url: "volunteers/benoit-chesneau.jpg",
    id: "a62f4db4-54d7-4fa3-aa8d-b141407d7cf1",
    member_id: "89ae528d-082c-4ebc-8ae5-22f4b1e5190d",
    name: "Benoit Chesneau",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "22761e5f-abb6-4e3c-b94f-a49a860ceeec",
    member_id: "715d8622-96fc-46e1-b9bb-45f41746ec65",
    name: "Robert Virding"
  },
  %{
    avatar_url: "volunteers/amos-king.png",
    id: "005264af-6639-4981-81b1-b72eec701b63",
    member_id: "753ceae2-6715-4e60-82d1-46a6bad15a07",
    name: "Amos King"
  },
  %{
    avatar_url: "volunteers/magdalena-pokorska.jpg",
    id: "c97c6ca3-5907-4995-bf39-0a9c329a4ce3",
    member_id: "f8bcc99f-195c-479f-90ed-be10d3473baf",
    name: "Magdalena Pokorska"
  },
  %{
    avatar_url: "volunteers/ghislaine-guerin.png",
    id: "63923553-8569-4c6a-9bbb-4997bc6cefe4",
    member_id: "171ed4ca-0960-4344-aab0-cc944d2801e1",
    name: "Ghislaine Guerin"
  },
  %{
    avatar_url: "volunteers/matt-mckinney.jpg",
    id: "3902e0c8-a660-4e80-8797-a9fb9475965d",
    member_id: "d76dab06-edae-49ec-8d42-b76d75bcd20b",
    name: "Matt McKinney"
  },
  %{
    avatar_url: "volunteers/beau-heubach.jpg",
    id: "9c8671fa-5714-4efb-95ce-d83ba55bb76d",
    member_id: "9fc3d4f3-39bf-49fc-979b-5fc3c57ec58d",
    name: "Beau Heubach"
  },
  %{
    avatar_url: "volunteers/lukasz-niemier.jpg",
    id: "ec44c3e5-e503-4646-b238-5f44a7fb6705",
    member_id: "0d8d5987-14df-4dd1-b463-4d58278144d1",
    name: "Łukasz Niemier"
  },
  %{
    avatar_url: "volunteers/vince-foley.png",
    id: "ca17f166-c032-49e6-812c-a7c9ce0e8687",
    member_id: "71e19bb7-cd69-40e2-a974-8847553902c7",
    name: "Vince Foley"
  },
  %{
    avatar_url: "volunteers/ilya-khaprov.jpg",
    id: "1b17cab9-3c67-46db-af13-73426fe9d8fe",
    member_id: "b3a7e33f-7b63-41f0-917d-966e4f9a5971",
    name: "Ilya Khaprov"
  },
  %{
    avatar_url: "volunteers/bryan-naegele.png",
    id: "307ea99d-69b8-435f-b44e-96f7e56157f3",
    member_id: "3d904c56-6d03-4ece-8890-2be154bde50e",
    name: "Bryan Naegele"
  },
  %{
    avatar_url: "volunteers/greg-mefford.jpg",
    id: "74a920c8-cad1-4198-b8eb-ea34cb9a0d79",
    member_id: "0004d845-97d4-4dca-bb31-d034323e27ad",
    name: "Greg Mefford"
  },
  %{
    avatar_url: "volunteers/arkadiusz-gil.jpg",
    id: "e857ef17-f14a-4f4e-99ad-2111bdeffbfc",
    member_id: "c48f6040-ed22-45bd-862e-0e39122c6472",
    name: "Arkadiusz Gil"
  },
  %{
    avatar_url: "volunteers/zachary-daniel.jpg",
    id: "441f27f4-eb92-4655-b6bd-f1bd51cc4bc8",
    member_id: "1b6ec21b-354b-4685-9078-b64c6916075e",
    name: "Zach Daniel"
  },
  %{
    avatar_url: "volunteers/mark-allen.jpg",
    id: "153a6416-51d0-4b92-811e-48b8e60b8a52",
    member_id: "6ffdc3fe-a50a-46bf-807f-92a265d7f75c",
    name: "Mark Allen"
  },
  %{
    avatar_url: "volunteers/andrew-thompson.jpg",
    id: "534c7df4-9777-409c-a31c-918c96c79d1e",
    member_id: "b75b3a4a-a022-4136-b8f1-e89de646ed6b",
    name: "Andrew Thompson"
  },
  %{
    avatar_url: "volunteers/bram-verburg.jpg",
    id: "53cc9c5d-6730-4e15-8fdf-dae7bc258478",
    member_id: "bfa8de43-e97a-400b-9cb2-a7ad87ad3d0d",
    name: "Bram Verburg"
  },
  %{
    avatar_url: "volunteers/alexandre-rodrigues.png",
    id: "719be561-4abc-4806-9f77-83b694275cfb",
    member_id: "76d17595-1153-4f70-b91c-a802279e98d2",
    name: "Alexandre Rodrigues"
  },
  %{
    avatar_url: nil,
    id: "ee6892ea-d5fc-425e-98cc-baea5a5bdb5b",
    member_id: "0f37d230-85bc-4daa-a7cd-a1d3d11ac570",
    name: "Hans Nilsson"
  },
  %{
    avatar_url: nil,
    id: "d5101178-aef7-420e-a70c-bd12a64076d9",
    member_id: "3fbfbc1b-b041-4e91-96f8-7af49cd7cde5",
    name: "Peter Dimitrov"
  },
  %{
    avatar_url: nil,
    id: "baef8df4-cfef-41cd-bbeb-a7752d3bd485",
    member_id: "b4e6e9c4-f8ab-4bd7-a16a-569d076236eb",
    name: "Griffin Byatt"
  },
  %{
    avatar_url: nil,
    id: "7d549c2c-b713-40d6-9a77-55b924aeedb3",
    member_id: "faa22482-a072-4071-ba67-efa33f1f01aa",
    name: "Duncan Sparrell"
  },
  %{
    avatar_url: "volunteers/drew-varner.jpg",
    id: "ace315ab-cc1e-4e09-9f33-96ad28e243e3",
    member_id: "cbbf6ea8-707f-4453-aaf5-01e9ec12b495",
    name: "Drew Varner"
  },
  %{
    avatar_url: "volunteers/sebastian-strollo.jpg",
    id: "e0f0666b-cbfb-43ae-9162-4bb3190af018",
    member_id: "88cfbc2b-e9d8-4849-b428-4f9cfb618216",
    name: "Sebastian Strollo",
    is_board_member: true
  },
  %{
    id: Ecto.UUID.generate(),
    avatar_url: "volunteers/richard-carlsson.jpg",
    name: "Richard Carlsson"
  }
  ]

Erlef.Seeds.insert(Erlef.Groups.Volunteer, vols)

vols = [
  %{
    avatar_url: "volunteers/tristan-sloughter.jpg",
    id: "4a9698ea-6bb2-4331-8846-0248246ab346",
    member_id: "e666dc26-9085-4f8d-a4c2-8bbada4905e7",
    name: "Tristan Sloughter"
  },
  %{
    avatar_url: "volunteers/fred-hebert.png",
    id: "8d8d7d05-cc79-445a-8e37-c56525024f7e",
    member_id: "69be5f73-fcae-4844-adbb-7de0d2d613b3",
    name: "Fred Hebert",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/bryan-paxton.jpg",
    id: "2641e9f6-6f2d-497d-b0ab-4f9f4d033115",
    member_id: "37370312-7e08-4a7f-b921-7bb9daafa956",
    name: "Bryan Paxton"
  },
  %{
    avatar_url: "volunteers/todd-resudek.jpg",
    id: "3d208e24-68dd-4d05-b2c9-e1fcbf8bb6d9",
    member_id: "1bb949e0-649d-4a41-883b-dceed1fa1d99",
    name: "Todd Resudek"
  },
  %{
    avatar_url: "volunteers/randy-thompson.jpg",
    id: "2565fbd1-dc33-4258-93a0-358f1b0a2cfd",
    member_id: "fe65ceeb-dc26-4daa-a4f3-62654379b556",
    name: "Randy Thompson"
  },
  %{
    avatar_url: "volunteers/wojtek-mach.jpg",
    id: "72491fc9-8c24-45a9-8119-2cd12fdae830",
    member_id: "71efaa52-a8ad-4bcc-af36-52fb20fe37c7",
    name: "Wojtek Mach"
  },
  %{
    avatar_url: nil,
    id: "e2d3bf92-50ae-422f-bcda-11aa62cebc47",
    member_id: "236d2723-1bdb-4711-8c1c-daa166d724fb",
    name: "Ivan Glushkov"
  },
  %{
    avatar_url: "volunteers/andrea-leopardi.jpg",
    id: "faab4673-f41b-4784-81c2-39eaa1b3d658",
    member_id: "8b79dadc-0c4c-4ed0-90b6-8967cb743249",
    name: "Andrea Leopardi"
  },
  %{
    avatar_url: nil,
    id: "29475e68-a75d-479d-8f3d-c35d2ade468b",
    member_id: "1a17a6c0-acce-4b96-b6de-a058adf8bc1d",
    name: "Justin Wood"
  },
  %{
    avatar_url: "volunteers/jose-valim.jpg",
    id: "affdba10-18a8-4402-9847-fe1976166dc3",
    member_id: "84e26a9b-ddf5-4690-97ee-742afb48a689",
    name: "José Valim",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "0abe96bb-690c-4b27-b978-ff8d8772321a",
    member_id: "9fa2e441-80fe-40c8-9836-a42030ed6d7d",
    name: "Thomas Depierre"
  },
  %{
    avatar_url: "volunteers/adam-lindberg.jpg",
    id: "8f22ac12-badc-4a71-80fa-4de70051ee2b",
    member_id: "feea4a07-2128-40a1-ad0a-df3a7a79bdb1",
    name: "Adam Lindberg"
  },
  %{
    avatar_url: "volunteers/maxim-fedorov.jpg",
    id: "4060d400-7b66-4724-ad16-2f813a94c8f7",
    member_id: "e9d55e5a-8337-4f31-8224-ff343013072d",
    name: "Maxim Fedorov",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "8a05166f-b2ec-466c-9f49-a4b148947d4a",
    member_id: "9ca23638-5855-4234-88f3-e0cdb5da849f",
    name: "John Hogberg"
  },
  %{
    avatar_url: "volunteers/kenneth-lundin.jpg",
    id: "9c129b15-b008-4947-b98b-def281e4535d",
    member_id: "a1fb3069-3597-4427-9ed8-c48f7624b096",
    name: "Kenneth Lundin",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/mariano-guerra.jpg",
    id: "6abaa50e-7b7a-4a1f-afcf-c2ad999c97ab",
    member_id: "62f2a89e-c773-47d5-b9e0-ca3beaea4507",
    name: "Mariano Guerra"
  },
  %{
    avatar_url: "volunteers/radek-szymczyszyn.jpg",
    id: "8fc7330f-72ef-4aba-a57f-2a7658453ad7",
    member_id: "ff80f5fa-bf24-4381-9629-f5fa0727bcdb",
    name: "Radosław Szymczyszyn"
  },
  %{
    avatar_url: "volunteers/laszlo-bacsi.png",
    id: "aa8e84d5-a63a-48bd-a5a3-c249bd8f99f1",
    member_id: "b24a9d17-fe71-4306-bf10-156f95475517",
    name: "László Bácsi"
  },
  %{
    avatar_url: "volunteers/brujo-benavides.png",
    id: "bdbd55e3-e8ab-41ee-9fac-746af27e7f92",
    member_id: "1a6b1515-4397-46fb-b4cb-361a7171893e",
    name: "Brujo Benavides"
  },
  %{
    avatar_url: "volunteers/francesco-cesarini.jpg",
    id: "589b130f-0ae5-410c-b1d1-3fcaf55d0f6f",
    member_id: "a5c4efd3-e235-4bf4-85f5-57b18a1a0446",
    name: "Francesco Cesarini",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/miriam-pena.jpg",
    id: "305cf09b-7cc0-4a29-b02c-d47301a762fb",
    member_id: "9cb09897-e979-4263-82e6-310b78429dc4",
    name: "Miriam Pena",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/peer-stritzinger.jpg",
    id: "a3fe00bc-e734-4f84-8e72-92c5fef519e9",
    member_id: "8e905dc9-e7d1-45c4-9844-68a5225fb474",
    name: "Peer Stritzinger",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/simon-thompson.jpg",
    id: "b28e5e4a-1480-49c5-90c8-5406654c7095",
    member_id: "5d5a10a2-4922-48e4-8bd8-1f2a682180e5",
    name: "Simon Thompson"
  },
  %{
    avatar_url: "volunteers/jeff-grunewald.jpg",
    id: "aed2f5f9-cf21-4a9f-9302-a732c980e038",
    member_id: "b71b30ab-b6d0-4ccb-b6cd-fa24f3964972",
    name: "Jeff Grunewald"
  },
  %{
    avatar_url: "volunteers//anna-neyzberg.jpg",
    id: "f9e4d9a9-d8c1-4f99-9008-1a4a82d8821b",
    member_id: "758f211d-4558-4430-9350-ba65025cc86e",
    name: "Anna Neyzberg"
  },
  %{
    avatar_url: "volunteers/laura-castro.jpg",
    id: "62fb737d-239d-4139-817c-1c907fabb273",
    member_id: "0c0258a2-cc67-4a4a-b86b-f36b75e7c21a",
    name: "Laura M. Castro"
  },
  %{
    avatar_url: "volunteers/mrinal-wadhwa.png",
    id: "9af5a1f6-2f1f-4210-8d66-4743c5f4489b",
    member_id: "ff6606c1-631d-48a2-934c-2ea01a900fec",
    name: "Mrinal Wadhwa"
  },
  %{
    avatar_url: "volunteers/ben-marx.jpg",
    id: "2cf7ff99-6d98-4a54-8dc6-d63c0797af0a",
    member_id: "cf74b2b1-c51f-42d0-b6ec-425954c75cd5",
    name: "Ben Marx"
  },
  %{
    avatar_url: "volunteers/alistair-woodman.png",
    id: "c8eb633a-4acd-4f8d-8ff2-2a25892e439b",
    member_id: "8b16bbfa-eeb4-4897-94a7-6a8da94ee522",
    name: "Alistair Woodman",
    is_board_member: true
  },
  %{
    avatar_url: "volunteers/paulo-d-gonzalez.png",
    id: "dff72785-a7f3-4a12-9e10-3475924d3969",
    member_id: "89f4873e-8d55-4154-af28-0c68c53512e4",
    name: "Paulo D. Gonzalez"
  },
  %{
    avatar_url: "volunteers/norberto-ortigoza.jpg",
    id: "9dbcf746-740d-48fa-85de-52d28b8e0f5e",
    member_id: "8e1c0df6-662d-4030-ae4e-8ad9f189dd5d",
    name: "Norberto Ortigoza"
  },
  %{
    avatar_url: "volunteers/adolfo-neto.jpg",
    id: "cbb6ce77-6d02-4ab2-9211-3d59e363b557",
    member_id: "8252209a-36dc-478b-9e81-eb103ee4ecc1",
    name: "Adolfo Neto"
  },
  %{
    avatar_url: "volunteers/johnny-winn.jpeg",
    id: "4bb5466e-440e-4424-b692-2e8fc42fb039",
    member_id: "8ec02dcd-56ee-42cb-832a-5bdc7cae3c59",
    name: "Johnny Winn"
  },
  %{
    avatar_url: "volunteers/raphael-vidal.jpeg",
    id: "352ea96b-6420-4351-827b-bfb37ea0634b",
    member_id: "a7c7e112-1727-4cc0-8806-ec7cff41718b",
    name: "Raphael Vidal"
  },
  %{
    avatar_url: "volunteers/annette-bieniusa.jpeg",
    id: "481982ea-5a7e-46ea-92a3-e0fdc95a9181",
    member_id: "848018bf-9ddf-4d29-8eca-171fd527ed58",
    name: "Annette Bieniusa"
  },
  %{
    avatar_url: "volunteers/bruno-azenha.jpeg",
    id: "0f28fdaa-4860-4f83-8c63-169982276a11",
    member_id: "8b01db41-3603-4277-9623-cb4ec1fde90b",
    name: "Bruno Azenha Gonçalves"
  },
  %{
    avatar_url: "volunteers/eric-douglas.jpeg",
    id: "d8ea3abc-d647-4475-b5dc-c1fbea1378d8",
    member_id: "d290e600-4e5d-4065-aeda-969fe4c10805",
    name: "Eric Douglas"
  },
  %{
    avatar_url: nil,
    id: "852e7313-0fff-40fa-a4a3-f505aadcd4df",
    member_id: "34c27e55-6b34-4dfa-ab43-7efb46a9ec22",
    name: "Frank Hunleth"
  },
  %{
    avatar_url: "volunteers/justin-schneck.jpg",
    id: "ee9779a3-e48f-4bf5-b47d-f60321ff19aa",
    member_id: "985ae4fa-0c74-4991-ab23-7ea4b7b3d3db",
    name: "Justin Schneck"
  },
  %{
    avatar_url: "volunteers/hideki-takase.jpg",
    id: "055cbfc1-156b-4c48-b274-696176204b49",
    member_id: "3e1f0557-c5ee-453d-b96c-f052016f7b05",
    name: "Hideki Takase"
  },
  %{
    avatar_url: "volunteers/natalia_chechina.jpg",
    id: "68adf5c8-9564-490b-b364-cfe145ac38a6",
    member_id: "e0c2bc7a-5a6d-4ba0-a67a-1928af53e3e7",
    name: "Natalia Chechina"
  },
  %{
    avatar_url: "volunteers/susumu-yamazaki.jpg",
    id: "dc5c56fe-52ab-479f-97ed-4fc10c3a5d35",
    member_id: "febcf842-ebf5-4919-a2b0-a958c45f6ab2",
    name: "Susumu Yamazaki"
  },
  %{
    avatar_url: "volunteers/akash-hiremath.jpg",
    id: "2c95b096-91db-4da9-8633-dc118e5b434a",
    member_id: "a4557964-2a79-4f48-91ae-7a882a0905db",
    name: "Akash Hiremath"
  },
  %{
    avatar_url: "volunteers/graham-leva.jpg",
    id: "5f84e9dd-26b7-4f48-a4dc-6690f3ff6df2",
    member_id: "1b71f264-cf2d-442d-9a49-5ad238dc9a3d",
    name: "Graham Leva"
  },
  %{
    avatar_url: "volunteers/isaac-yonemoto.jpg",
    id: "8ecfd6ab-4c5f-42bc-adbf-9d5d54d59957",
    member_id: "6b687258-6418-4c07-bac1-34976c34476e",
    name: "Isaac Yonemoto"
  },
  %{
    avatar_url: "volunteers/benoit-chesneau.jpg",
    id: "a62f4db4-54d7-4fa3-aa8d-b141407d7cf1",
    member_id: "89ae528d-082c-4ebc-8ae5-22f4b1e5190d",
    name: "Benoit Chesneau",
    is_board_member: true
  },
  %{
    avatar_url: nil,
    id: "22761e5f-abb6-4e3c-b94f-a49a860ceeec",
    member_id: "715d8622-96fc-46e1-b9bb-45f41746ec65",
    name: "Robert Virding"
  },
  %{
    avatar_url: "volunteers/amos-king.png",
    id: "005264af-6639-4981-81b1-b72eec701b63",
    member_id: "753ceae2-6715-4e60-82d1-46a6bad15a07",
    name: "Amos King"
  },
  %{
    avatar_url: "volunteers/magdalena-pokorska.jpg",
    id: "c97c6ca3-5907-4995-bf39-0a9c329a4ce3",
    member_id: "f8bcc99f-195c-479f-90ed-be10d3473baf",
    name: "Magdalena Pokorska"
  },
  %{
    avatar_url: "volunteers/ghislaine-guerin.png",
    id: "63923553-8569-4c6a-9bbb-4997bc6cefe4",
    member_id: "171ed4ca-0960-4344-aab0-cc944d2801e1",
    name: "Ghislaine Guerin"
  },
  %{
    avatar_url: "volunteers/matt-mckinney.jpg",
    id: "3902e0c8-a660-4e80-8797-a9fb9475965d",
    member_id: "d76dab06-edae-49ec-8d42-b76d75bcd20b",
    name: "Matt McKinney"
  },
  %{
    avatar_url: "volunteers/beau-heubach.jpg",
    id: "9c8671fa-5714-4efb-95ce-d83ba55bb76d",
    member_id: "9fc3d4f3-39bf-49fc-979b-5fc3c57ec58d",
    name: "Beau Heubach"
  },
  %{
    avatar_url: "volunteers/lukasz-niemier.jpg",
    id: "ec44c3e5-e503-4646-b238-5f44a7fb6705",
    member_id: "0d8d5987-14df-4dd1-b463-4d58278144d1",
    name: "Łukasz Niemier"
  },
  %{
    avatar_url: "volunteers/vince-foley.png",
    id: "ca17f166-c032-49e6-812c-a7c9ce0e8687",
    member_id: "71e19bb7-cd69-40e2-a974-8847553902c7",
    name: "Vince Foley"
  },
  %{
    avatar_url: "volunteers/ilya-khaprov.jpg",
    id: "1b17cab9-3c67-46db-af13-73426fe9d8fe",
    member_id: "b3a7e33f-7b63-41f0-917d-966e4f9a5971",
    name: "Ilya Khaprov"
  },
  %{
    avatar_url: "volunteers/bryan-naegele.png",
    id: "307ea99d-69b8-435f-b44e-96f7e56157f3",
    member_id: "3d904c56-6d03-4ece-8890-2be154bde50e",
    name: "Bryan Naegele"
  },
  %{
    avatar_url: "volunteers/greg-mefford.jpg",
    id: "74a920c8-cad1-4198-b8eb-ea34cb9a0d79",
    member_id: "0004d845-97d4-4dca-bb31-d034323e27ad",
    name: "Greg Mefford"
  },
  %{
    avatar_url: "volunteers/arkadiusz-gil.jpg",
    id: "e857ef17-f14a-4f4e-99ad-2111bdeffbfc",
    member_id: "c48f6040-ed22-45bd-862e-0e39122c6472",
    name: "Arkadiusz Gil"
  },
  %{
    avatar_url: "volunteers/zachary-daniel.jpg",
    id: "441f27f4-eb92-4655-b6bd-f1bd51cc4bc8",
    member_id: "1b6ec21b-354b-4685-9078-b64c6916075e",
    name: "Zach Daniel"
  },
  %{
    avatar_url: "volunteers/mark-allen.jpg",
    id: "153a6416-51d0-4b92-811e-48b8e60b8a52",
    member_id: "6ffdc3fe-a50a-46bf-807f-92a265d7f75c",
    name: "Mark Allen"
  },
  %{
    avatar_url: "volunteers/andrew-thompson.jpg",
    id: "534c7df4-9777-409c-a31c-918c96c79d1e",
    member_id: "b75b3a4a-a022-4136-b8f1-e89de646ed6b",
    name: "Andrew Thompson"
  },
  %{
    avatar_url: "volunteers/bram-verburg.jpg",
    id: "53cc9c5d-6730-4e15-8fdf-dae7bc258478",
    member_id: "bfa8de43-e97a-400b-9cb2-a7ad87ad3d0d",
    name: "Bram Verburg"
  },
  %{
    avatar_url: "volunteers/alexandre-rodrigues.png",
    id: "719be561-4abc-4806-9f77-83b694275cfb",
    member_id: "76d17595-1153-4f70-b91c-a802279e98d2",
    name: "Alexandre Rodrigues"
  },
  %{
    avatar_url: nil,
    id: "ee6892ea-d5fc-425e-98cc-baea5a5bdb5b",
    member_id: "0f37d230-85bc-4daa-a7cd-a1d3d11ac570",
    name: "Hans Nilsson"
  },
  %{
    avatar_url: nil,
    id: "d5101178-aef7-420e-a70c-bd12a64076d9",
    member_id: "3fbfbc1b-b041-4e91-96f8-7af49cd7cde5",
    name: "Peter Dimitrov"
  },
  %{
    avatar_url: nil,
    id: "baef8df4-cfef-41cd-bbeb-a7752d3bd485",
    member_id: "b4e6e9c4-f8ab-4bd7-a16a-569d076236eb",
    name: "Griffin Byatt"
  },
  %{
    avatar_url: nil,
    id: "7d549c2c-b713-40d6-9a77-55b924aeedb3",
    member_id: "faa22482-a072-4071-ba67-efa33f1f01aa",
    name: "Duncan Sparrell"
  },
  %{
    avatar_url: "volunteers/drew-varner.jpg",
    id: "ace315ab-cc1e-4e09-9f33-96ad28e243e3",
    member_id: "cbbf6ea8-707f-4453-aaf5-01e9ec12b495",
    name: "Drew Varner"
  },
  %{
    avatar_url: "volunteers/sebastian-strollo.jpg",
    id: "e0f0666b-cbfb-43ae-9162-4bb3190af018",
    member_id: "88cfbc2b-e9d8-4849-b428-4f9cfb618216",
    name: "Sebastian Strollo",
    is_board_member: true
  },
  %{
    id: Ecto.UUID.generate(),
    avatar_url: "volunteers/richard-carlsson.jpg",
    name: "Richard Carlsson",
    is_board_member: true
  }
  ]

Erlef.Seeds.insert(Erlef.Groups.Volunteer, vols)