Oddities
Summary
This notebook is primarily a place to consolidate awkward behavior I’m not used to with the various LiveBook notebooks I’ve used.
Zero-width Space
The zero-width space typically shows up when copying and pasting from websites as I’ve done below. Being a zero-width space, you cannot see what line the problem occurs in.
You will likely an error message like
** (SyntaxError) nofile:5:1: unexpected token: "" (column 1, code point U+200B)
.
alias NimbleCSV.RFC4180, as: CSV
"./data/jobs.csv"
|> File.read!()
|> CSV.parse_string(skip_headers: false)
#|> Enum.sort_by(&(&1.date), {:desc, Date})
nofile
is typical because LiveBook isn’t compiling an Elixir file from disk.
5
is the line.
1
is the first character.
If we set our cursor before the |>
we can hit backspace and watch it remove the zero-width space.
The section below now reports an error we’re aware of, that “./data/jobs.csv” does not (currently) exist.
alias NimbleCSV.RFC4180, as: CSV
"./data/jobs.csv"
|> File.read!()
|> CSV.parse_string(skip_headers: false)
# |> Enum.sort_by(&(&1.date), {:desc, Date})
Model Attributes
This first code cell is the “broken” version pulled from my attempt at the DockYard Academy Product Filters
exercise.
We’ve skipped the other tests as they only add noise when this triggers an error.
With the broken declaration we see the following warning types when running the tests.
warning: module attribute @products in code block has no effect as it is never returned (remove the attribute or assign it to _ to avoid warnings)
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:95: ProductsTest
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:95: ProductsTest (module)
We also see the error:
1) test filter/2 by exact matching name (ProductsTest)
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:109
** (Protocol.UndefinedError) protocol Enumerable not implemented for nil of type Atom
stacktrace:
(elixir 1.14.0) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.14.0) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.14.0) lib/enum.ex:4307: Enum.filter/2
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:67: Products.filter/2
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:110: (test)
How can the code trigger the warnings and yet the following test passes?
test "filter/2 empty filters" do
assert Products.filter(@products, []) == @products
end
There are no nil
or Atom
checks with the function head def filter(products, []), do: products
so this somewhat makes sense.
The example solution (commented out) triggers the ** (Protocol.UndefinedError) protocol Enumerable not implemented for nil of type Atom
error.
What the Problem Could be
My guess is the use of product.name
triggers the error because product
is an empty struct. That hypothesis is completely wrong.
After placing an IO.inspect(products, label: "Products")
we see the real problem.
The enum we expect products
to be is in fact nil
.
This makes absolute sense when you begin to work with enumerables more often to see the pattern.
Correcting the Problem
The “fix” is to change
@products
[
%{name: "Laptop", category: :tech, price: 100_000},
%{name: "Phone", category: :tech, price: 50000},
%{name: "Chocolate", category: :snacks, price: 200},
%{name: "Shampoo", category: :health, price: 1000}
]
to
@products [
%{name: "Laptop", category: :tech, price: 100_000},
%{name: "Phone", category: :tech, price: 50000},
%{name: "Chocolate", category: :snacks, price: 200},
%{name: "Shampoo", category: :health, price: 1000}
]
This is extremely subtle, that the opening bracket [
should be on the line with the attribute declaration.Ï
defmodule Products do
@moduledoc """
Documentation for `Products`
"""
@doc """
Filter products by name, category, and price.
## Examples
No filters returns all products.
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], [])
[%{name: "Laptop", category: :tech, price: 100}]
Filter by name
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], name: "Laptop")
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], name: "apt")
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], name: "APT")
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], name: "Phone")
[]
Filter by category
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], category: :tech)
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], category: :health)
[]
Filter by min price.
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], min: 100)
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], min: 50)
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], min: 200)
[]
Filter by max price.
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], max: 100)
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], max: 200)
[%{name: "Laptop", category: :tech, price: 100}]
iex> Products.filter([%{name: "Laptop", category: :tech, price: 100}], max: 50)
[]
Multiple filters.
Products.filter([%{name: "Laptop", category: :tech, price: 100}], min: 50, max: 200, name: "Laptop", category: :tech)
"""
def filter(products, []), do: products
def filter(products, filters) do
IO.inspect(products, label: "Products")
Enum.filter(products, fn product ->
lowercase = String.downcase(product.name)
String.contains?(lowercase, String.downcase(filters[:name] || ""))
end)
|> Enum.filter(fn product ->
product.category == filters[:category] || filters[:category] == nil
end)
|> Enum.filter(fn product ->
product.price > filters[:min] || filters[:min] == nil
end)
|> Enum.filter(fn product ->
product.price < filters[:max] || filters[:max] == nil
end)
end
# Example solution
# def filter(products, filters) do
# name_filter = Keyword.get(filters, :name, "")
# category_filter = Keyword.get(filters, :category)
# min_filter = Keyword.get(filters, :min)
# max_filter = Keyword.get(filters, :max)
# products
# |> Enum.filter(fn product ->
# matches_name =
# !name_filter or String.contains?(String.downcase(product.name), String.downcase(name_filter))
# matches_category = !category_filter or product.category == category_filter
# above_min_price = !min_filter or min_filter <= product.price
# below_max_price = !max_filter or product.price <= max_filter
# matches_name and matches_category and above_min_price and below_max_price
# end)
# end
end
ExUnit.start(auto_run: false)
defmodule ProductsTest do
use ExUnit.Case
# @products [
# %{name: "Laptop", category: :tech, price: 100_000},
# %{name: "Phone", category: :tech, price: 50000},
# %{name: "Chocolate", category: :snacks, price: 200},
# %{name: "Shampoo", category: :health, price: 1000}
# ]
@products
[
%{name: "Laptop", category: :tech, price: 100_000},
%{name: "Phone", category: :tech, price: 50000},
%{name: "Chocolate", category: :snacks, price: 200},
%{name: "Shampoo", category: :health, price: 1000}
]
test "filter/2 empty filters" do
assert Products.filter(@products, []) == @products
end
# TODO: Comment this out to see the test pass in spite of the warnings
# @tag :skip
test "filter/2 by exact matching name" do
assert Products.filter(@products, name: "Laptop") ==
[%{name: "Laptop", category: :tech, price: 100_000}]
end
@tag :skip
test "filter/2 by partial matching name" do
assert Products.filter(@products, name: "Lap") ==
[%{name: "Laptop", category: :tech, price: 100_000}]
end
@tag :skip
test "filter/2 by mixed case partial matching name" do
assert Products.filter(@products, name: "LaPtOp") ==
[%{name: "Laptop", category: :tech, price: 100_000}]
end
@tag :skip
test "filter/2 by category" do
assert Products.filter(@products, category: :tech) ==
[
%{name: "Laptop", category: :tech, price: 100_000},
%{name: "Phone", category: :tech, price: 50000}
]
end
@tag :skip
test "filter/2 by min price" do
assert Products.filter(@products, min: 201) == [
%{name: "Laptop", category: :tech, price: 100_000},
%{name: "Phone", category: :tech, price: 50000},
%{name: "Shampoo", category: :health, price: 1000}
]
end
@tag :skip
test "filter/2 by max price" do
assert Products.filter(@products, max: 2000) == [
%{name: "Chocolate", category: :snacks, price: 200},
%{name: "Shampoo", category: :health, price: 1000}
]
end
@tag :skip
test "filter/2 by max and min price" do
assert Products.filter(@products, min: 201, max: 80000) == [
%{name: "Phone", category: :tech, price: 50000},
%{name: "Shampoo", category: :health, price: 1000}
]
end
@tag :skip
test "filter/2 all filters" do
assert Products.filter(@products, name: "T", category: :snacks, min: 201, max: 150_000) ==
[]
end
end
ExUnit.run()
warning: module attribute @products in code block has no effect as it is never returned (remove the attribute or assign it to _ to avoid warnings)
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:116: ProductsTest
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:116: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:125: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:125: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:131: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:137: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:143: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:149: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:158: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:167: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:175: ProductsTest (module)
warning: undefined module attribute @products, please remove access to @products or explicitly set it before access
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:183: ProductsTest (module)
*Products: nil
******
1) test filter/2 by exact matching name (ProductsTest)
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:130
** (Protocol.UndefinedError) protocol Enumerable not implemented for nil of type Atom
stacktrace:
(elixir 1.14.0) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.14.0) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.14.0) lib/enum.ex:4307: Enum.filter/2
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:69: Products.filter/2
/Users/Shared/repositories/personal/elixir/livebook_notebooks/oddities.livemd#cell:43lgu757mntkvawnumdyouaotky5zv6l:131: (test)
.
Finished in 0.00 seconds (0.00s async, 0.00s sync)
9 tests, 1 failure, 7 skipped
Randomized with seed 649338
%{excluded: 0, failures: 1, skipped: 7, total: 9}