Strings are UTF-8 encoded.
Triple-quotes used for heredocs function slightly differently than Python’s triple quotes. Python keeps the initial linebreak unless the first line has
\
. Elixir heredocs never keep the initial linebreak.A magīa of sigils.
- Case indicates if the text is to be interpolated (uppercase) or not (lowercase).
- Multiple delimiter choices.
- Some sigils have optional specifiers.
Double quotes declare strings, which are binary lists. Single quotes declare character lists. Vastly different in the Elixir world.
Elixir prints a list of integers as a string if each number in the list is a printable
codepoint
, or character. For example:[6]
displays as[6]
but[8]
displays as ‘\b’ (bell).[126]
displays as~
but[127]
displays as[127]
.
The
next_codepoint
example shows how to create a byte list iterator.I became more comfortable with using pattern matching and guard clauses at the function level vs. if/else clauses.
Exercises
StringsAndBinaries-1
Write a function that returns true if a single-quoted string contains only printable ASCII characters (space through tilde).
defmodule Ch11 do
defp is_printable?([], _) do
true
end
defp is_printable?([head | tail], range) do
if head in range do
is_printable?(tail, range)
else
raise "Not printable '#{head}'."
end
end
def printable?(clist) do
is_printable?(clist, 32..127)
end
end
Ch11.printable?('123')
Ch11.printable?('12ô')
StringsAndBinaries-2
Write an anagram?(word1, word2)
that returns true if its parameters are anagrams.
- I’m assuming this is for character lists, not strings.
- Must have identical collection of characters.
defmodule Ch11 do
def anagram?(word1, word2) do
Enum.sort(word1) == Enum.sort(word2)
end
end
Ch11.anagram?('abc', 'cba')
StringsAndBinaries-3
Why does IEx print 'cat'
as a string, but 'dog'
as individual numbers?
- Because the head is a character list of printable characters and the entire list is not (since it has a sublist).
StringsAndBinaries-4
Write a function that takes a single-quoted string of the form number [+-*/] number
and returns the result of the calculation.
The individual numbers do not have leading plus or minus signs.
- Cannot use
String.split()
with character lists. - Does not try to identify an invalid equation.
- The reason for
?x
notation makes much more sense after this exercise. - Use an Erlang module to convert character list to integer.
defmodule Ch11 do
defp calc([?+ | tail], agg) do
{ first, _ } = :string.to_integer(Enum.reverse(agg))
{ second, _ } = :string.to_integer(tail)
IO.inspect(tail)
first + second
end
defp calc([?- | tail], agg) do
{ first, _ } = :string.to_integer(Enum.reverse(agg))
{ second, _ } = :string.to_integer(tail)
first - second
end
defp calc([?* | tail], agg) do
{ first, _ } = :string.to_integer(Enum.reverse(agg))
{ second, _ } = :string.to_integer(tail)
first * second
end
defp calc([?/ | tail], agg) do
{ first, _ } = :string.to_integer(Enum.reverse(agg))
{ second, _ } = :string.to_integer(tail)
first / second
end
defp calc([head | tail], agg) do
calc(tail, [head | agg])
end
def calculate(equation) do
calc(equation, [])
end
end
Ch11.calculate('10+3')
Compared to Dave’s
- created number and operator parsers that skipped spaces;
- created an anonymous function to perform the math;
- used
div
for integer division.
StringsAndBinaries-5
Write a function that takes a list of double-quoted strings and prints each on a separate line, centered in a column that has the width of the longest string. Make sure it works with UTF characters.
defmodule Ch11 do
defp _longest([], length) do
length
end
defp _longest([head | tail], length) do
if String.length(head) > length do
_longest(tail, String.length(head))
else
_longest(tail, length)
end
end
defp _centered([], _) do
end
defp _centered([head | tail], padding) do
head_length = div(String.length(head), 2)
base = String.duplicate(" ", padding - head_length)
IO.puts(base <> head)
_centered(tail, padding)
end
def center(list) do
longest = _longest(list, 0)
IO.puts("Maximum string length: #{longest}.")
# Want 1/2 of numbers up front.
_centered(list, div(longest, 2))
end
end
Ch11.center(["cat", "zebra", "elephant"])
Compared to Dave’s
- used a pipe’d approach
- created a tuple of string and string’s length so as to not calculate length twice.
- my approach is still to Python’ish. Need to start using piping more.
StringsAndBinaries-6
Write a function to capitalize the sentences in a string.
defmodule Ch11 do
defp until_period(<< head :: utf8, tail :: binary >>, agg) when head == ?. do
capitalize(tail, [head | agg])
end
defp until_period(<< head :: utf8, tail :: binary >>, agg) do
until_period(tail, [ head |agg ])
end
defp capitalize(<<>>, agg) do
:string.reverse(agg)
end
defp capitalize(<< head :: utf8, tail :: binary >>, agg) when head == 32 do
capitalize(tail, [:string.to_upper(head) | agg])
end
defp capitalize(<< head :: utf8, tail :: binary >>, agg) do
until_period(tail, [:string.to_upper(head) | agg])
end
def capitalize_sentences(sentences) when is_binary(sentences) do
# First character is start of sentence.
# Ignore leading spaces as it is not in the problem definition.
capitalize(sentences, <<>>)
end
end
Ch11.capitalize_sentences("oh. a dog. woof.")
Compared to Dave’s
- I’m writing much more code;
- done again with pipe and using
String.split
; - I interpreted problem as working with bytes. Dave uses String module.
StringsAndBinaries-7
Add tax to orders loaded from a file.
defmodule Ch11 do
def calculate_tax(path, tax_rates) do
read_orders(path)
|> Enum.map(fn order -> apply_tax(order, tax_rates) end )
end
def read_orders(path) do
file = File.open!(path)
[header | lines] = Enum.map(IO.stream(file, :line), &String.trim(&1))
header = String.split(header, ",")
header = Enum.map(header, &String.to_atom(&1))
Enum.map(lines, fn line -> parse_order(String.split(line, ","), header) end )
end
def parse_order(line, header) do
line = Enum.zip(header, line)
id = Keyword.get(line, :id)
line = Keyword.replace(line, :id, String.to_integer(id))
net_amount = Keyword.get(line, :net_amount)
line = Keyword.replace(line, :net_amount, String.to_float(net_amount))
ship_to = Keyword.get(line, :ship_to)
Keyword.replace(line, :ship_to, String.to_atom(ship_to))
end
def apply_tax(order, tax_rates) do
net_amount = Keyword.get(order, :net_amount)
ship_to = Keyword.get(order, :ship_to)
tax = Keyword.get(tax_rates, ship_to, 0)
Keyword.put(order, :total_amount, net_amount + (tax * net_amount))
end
end
tax_rates = [ NC: 0.075, TX: 0.08 ]
orders = Ch11.calculate_tax("orders.csv", tax_rates)
IO.inspect(orders)
Compared to Dave’s
- didn’t use stream, so not as efficient for large order files
All notes and comments are my own opinion. Follow me at @rgacote@genserver.social