Accept various date/time formats in CSV import
Parse US dates (6/15/2024 2:30 PM), 24h (06/15/2024 14:30), dashes (06-15-2024 14:30), compact (20240615 1430), date-only (2024-06-15), and ISO 8601 — all normalized to UTC before insert.
This commit is contained in:
parent
bc70b38e93
commit
05654289af
2 changed files with 85 additions and 7 deletions
|
|
@ -65,19 +65,94 @@ defmodule Microwaveprop.Radio.CsvImport do
|
|||
|> Map.new()
|
||||
|> Map.put("submitter_email", submitter_email)
|
||||
|
||||
case Radio.create_contact(attrs) do
|
||||
{:ok, contact} ->
|
||||
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||
{:ok, contact}
|
||||
case normalize_timestamp(attrs["qso_timestamp"]) do
|
||||
{:ok, iso_timestamp} ->
|
||||
attrs = Map.put(attrs, "qso_timestamp", iso_timestamp)
|
||||
|
||||
{:error, changeset} ->
|
||||
{:error, changeset_error_strings(changeset)}
|
||||
case Radio.create_contact(attrs) do
|
||||
{:ok, contact} ->
|
||||
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||
{:ok, contact}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:error, changeset_error_strings(changeset)}
|
||||
end
|
||||
|
||||
{:error, msg} ->
|
||||
{:error, [msg]}
|
||||
end
|
||||
else
|
||||
{:error, ["expected #{@expected_columns} columns, got #{length(fields)}"]}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses various date/time formats into ISO 8601 UTC strings.
|
||||
|
||||
Accepted formats:
|
||||
- ISO 8601: 2024-06-15T14:30:00Z, 2024-06-15 14:30:00Z, 2024-06-15T14:30:00
|
||||
- US date: 06/15/2024 14:30, 6/15/2024 2:30 PM
|
||||
- US date: 06-15-2024 14:30
|
||||
- EU date: 15/06/2024 14:30
|
||||
- Date only: 2024-06-15 (assumes 00:00 UTC)
|
||||
- Compact: 20240615 1430, 20240615T143000
|
||||
"""
|
||||
def normalize_timestamp(raw) do
|
||||
trimmed = String.trim(raw)
|
||||
|
||||
cond do
|
||||
# Already ISO 8601 with timezone
|
||||
match?({:ok, _, _}, DateTime.from_iso8601(trimmed)) ->
|
||||
{:ok, trimmed}
|
||||
|
||||
# ISO-ish without Z: 2024-06-15T14:30:00 or 2024-06-15 14:30:00
|
||||
Regex.match?(~r/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/, trimmed) ->
|
||||
{:ok, trimmed <> "Z"}
|
||||
|
||||
# Date only: 2024-06-15
|
||||
Regex.match?(~r/^\d{4}-\d{2}-\d{2}$/, trimmed) ->
|
||||
{:ok, trimmed <> "T00:00:00Z"}
|
||||
|
||||
# US format with AM/PM: 6/15/2024 2:30 PM or 06/15/2024 02:30 PM
|
||||
match = Regex.run(~r"^(\d{1,2})/(\d{1,2})/(\d{4})\s+(\d{1,2}):(\d{2})\s*(AM|PM)$"i, trimmed) ->
|
||||
[_, m, d, y, h, min, ampm] = match
|
||||
hour = parse_12h(String.to_integer(h), String.upcase(ampm))
|
||||
format_iso(y, m, d, hour, min)
|
||||
|
||||
# US format 24h: 6/15/2024 14:30 or 06/15/2024 14:30:00
|
||||
match = Regex.run(~r"^(\d{1,2})/(\d{1,2})/(\d{4})\s+(\d{1,2}):(\d{2})(:\d{2})?$", trimmed) ->
|
||||
[_, m, d, y, h, min | _] = match
|
||||
format_iso(y, m, d, String.to_integer(h), min)
|
||||
|
||||
# US format with dashes: 06-15-2024 14:30
|
||||
match = Regex.run(~r"^(\d{1,2})-(\d{1,2})-(\d{4})\s+(\d{1,2}):(\d{2})(:\d{2})?$", trimmed) ->
|
||||
[_, m, d, y, h, min | _] = match
|
||||
format_iso(y, m, d, String.to_integer(h), min)
|
||||
|
||||
# Compact: 20240615 1430 or 20240615T143000
|
||||
match = Regex.run(~r"^(\d{4})(\d{2})(\d{2})[T ]?(\d{2})(\d{2})(\d{2})?$", trimmed) ->
|
||||
[_, y, m, d, h, min | _] = match
|
||||
format_iso(y, m, d, String.to_integer(h), min)
|
||||
|
||||
true ->
|
||||
{:error, "qso_timestamp could not be parsed: #{trimmed}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_12h(12, "AM"), do: 0
|
||||
defp parse_12h(12, "PM"), do: 12
|
||||
defp parse_12h(h, "PM"), do: h + 12
|
||||
defp parse_12h(h, "AM"), do: h
|
||||
|
||||
defp format_iso(y, m, d, hour, min) do
|
||||
y = String.pad_leading(to_string(y), 4, "0")
|
||||
m = String.pad_leading(to_string(m), 2, "0")
|
||||
d = String.pad_leading(to_string(d), 2, "0")
|
||||
h = String.pad_leading(to_string(hour), 2, "0")
|
||||
min = String.pad_leading(to_string(min), 2, "0")
|
||||
{:ok, "#{y}-#{m}-#{d}T#{h}:#{min}:00Z"}
|
||||
end
|
||||
|
||||
defp changeset_error_strings(changeset) do
|
||||
changeset
|
||||
|> Ecto.Changeset.traverse_errors(fn {message, opts} ->
|
||||
|
|
|
|||
|
|
@ -261,7 +261,10 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
</p>
|
||||
<code class="text-xs">station1, station2, grid1, grid2, band, mode, qso_timestamp</code>
|
||||
<ul class="list-disc list-inside mt-2 space-y-1 text-base-content/60">
|
||||
<li>Timestamps must be ISO 8601 UTC, e.g. <code>2024-06-15T14:30:00Z</code></li>
|
||||
<li>
|
||||
Timestamps in most formats accepted (e.g. <code>2024-06-15T14:30:00Z</code>,
|
||||
<code>6/15/2024 2:30 PM</code>, <code>2024-06-15 14:30</code>). All times assumed UTC.
|
||||
</li>
|
||||
<li>Grid squares should be as detailed as possible (6 characters preferred, e.g. EM12kp)</li>
|
||||
<li>Band in MHz (e.g. 10000, 24000)</li>
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue