Current section
Files
Jump to
Current section
Files
test/airbnb_reservation_component.ex
defmodule Parselet.Test do
defmodule Date do
alias Timex
def normalize_range([check_in_str, check_out_str]) do
year = Date.utc_today().year
{:ok, check_in} =
Timex.parse("#{check_in_str} #{year}", "{Mshort} {D} {YYYY}")
|> to_date()
{:ok, check_out} =
Timex.parse("#{check_out_str} #{year}", "{Mshort} {D} {YYYY}")
|> to_date()
%{
check_in: check_in,
check_out: check_out
}
end
defp to_date({:ok, dt}), do: {:ok, Timex.to_date(dt)}
defp to_date(error), do: error
end
defmodule Components.AirbnbReservation do
use Parselet.Component
field :reservation_code,
pattern: ~r/Reservation code[:\s]+([A-Z0-9\-]+)/i,
capture: :first
field :guest_name,
pattern: ~r/(?:You’re hosting|You're hosting|Reservation for)\s+([^\n]+)/i,
capture: :first,
transform: &String.trim/1
field :date_range,
pattern: ~r/([A-Za-z]{3} \d{1,2})\s*[–-]\s*([A-Za-z]{3} \d{1,2})/,
capture: :all,
transform: &Parselet.Test.Date.normalize_range/1
field :nights,
pattern: ~r/(\d+)\s+nights?/i,
capture: :first,
transform: &String.to_integer/1
field :guest_count,
pattern: ~r/(\d+)\s+guests?/i,
capture: :first,
transform: &String.to_integer/1
field :listing_name,
function: fn text ->
lines =
text
|> String.split("\n", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
date_line = ~r/[A-Za-z]{3}\s+\d{1,2}\s+[–—-]\s+[A-Za-z]{3}\s+\d{1,2}.*\b\d+\s+nights?/i
lines
|> case do
[] -> nil
ls ->
case Enum.find_index(ls, &String.match?(&1, date_line)) do
nil ->
ls
|> Enum.find(fn line ->
String.contains?(line, ["BR", "Apartment", "House", "Gateway"]) and
not String.match?(line, ~r/^(Reservation|Reservation code|Check-?in|Checkout|Your earnings|Payout|\d+\s+guests?)/i)
end)
idx ->
ls
|> Enum.drop(idx + 1)
|> Enum.find(fn line ->
line not in [""] and
not String.match?(line, ~r/^(\d+\s+guests?|Check-?in|Checkout|Your earnings|Payout)/i)
end)
end
end
end
field :check_in_time,
pattern: ~r/Check-?in[:\s]+\s*([\d:]+\s*[AP]M)/i,
capture: :first
field :check_out_time,
pattern: ~r/Check-?out[:\s]+\s*([\d:]+\s*[AP]M)/i,
capture: :first
field :payout_amount,
pattern: ~r/(?:Your earnings|Payout)[:\s]+\$([\d,]+\.\d{2})/i,
capture: :first,
transform: fn amt ->
amt
|> String.replace(",", "")
|> String.to_float()
end
end
defmodule Parser do
use Parselet.Parser
@reservation_markers [
"You’re hosting",
"You're hosting",
"Reservation confirmed",
"Reservation for"
]
def parse(email_text) do
case classify(email_text) do
:reservation ->
Parselet.parse(email_text, components: [AirbnbReservation])
:unknown ->
{:error, :unknown_airbnb_email_type}
end
end
defp classify(text) do
if Enum.any?(@reservation_markers, &String.contains?(text, &1)) do
:reservation
else
:unknown
end
end
end
end