exercism / elixir

Exercism exercises in Elixir.
https://exercism.org/tracks/elixir
MIT License
625 stars 398 forks source link

Building a training set of tags for elixir #1386

Closed ErikSchierboom closed 12 months ago

ErikSchierboom commented 1 year ago

Hello lovely maintainers :wave:

We've recently added "tags" to student's solutions. These express the constructs, paradigms and techniques that a solution uses. We are going to be using these tags for lots of things including filtering, pointing a student to alternative approaches, and much more.

In order to do this, we've built out a full AST-based tagger in C#, which has allowed us to do things like detect recursion or bit shifting. We've set things up so other tracks can do the same for their languages, but its a lot of work, and we've determined that actually it may be unnecessary. Instead we think that we can use machine learning to achieve tagging with good enough results. We've fine-tuned a model that can determine the correct tags for C# from the examples with a high success rate. It's also doing reasonably well in an untrained state for other languages. We think that with only a few examples per language, we can potentially get some quite good results, and that we can then refine things further as we go.

I released a new video on the Insiders page that talks through this in more detail.

We're going to be adding a fully-fledged UI in the coming weeks that allow maintainers and mentors to tag solutions and create training sets for the neural networks, but to start with, we're hoping you would be willing to manually tag 20 solutions for this track. In this post we'll add 20 comments, each with a student's solution, and the tags our model has generated. Your mission (should you choose to accept it) is to edit the tags on each issue, removing any incorrect ones, and add any that are missing. In order to build one model that performs well across languages, it's best if you stick as closely as possible to the C# tags as you can. Those are listed here. If you want to add extra tags, that's totally fine, but please don't arbitrarily reword existing tags, even if you don't like what Erik's chosen, as it'll just make it less likely that your language gets the correct tags assigned by the neural network.


To summarise - there are two paths forward for this issue:

  1. You're up for helping: Add a comment saying you're up for helping. Update the tags some time in the next few days. Add a comment when you're done. We'll then add them to our training set and move forward.
  2. You not up for helping: No problem! Just please add a comment letting us know :)

If you tell us you're not able/wanting to help or there's no comment added, we'll automatically crowd-source this in a week or so.

Finally, if you have questions or want to discuss things, it would be best done on the forum, so the knowledge can be shared across all maintainers in all tracks.

Thanks for your help! :blue_heart:


Note: Meta discussion on the forum

ErikSchierboom commented 1 year ago

Exercise: acronym

Code

defmodule Acronym do
  @doc """
  Generate an acronym from a string.
  "This is a string" => "TIAS"
  """
  @spec abbreviate(string) :: String.t()
  def abbreviate(string), do: abbreviate(String.codepoints(string), "", [])
  defp abbreviate([], _prev, result), do: Enum.join(result) |> String.upcase
  defp abbreviate([head|tail], prev, result) do
    cond do
      String.match?(head, ~r{^[A-Z]}) ->
        abbreviate(tail, head, result ++ [head])
      String.match?(prev, ~r{\W}) && String.match?(head, ~r{^[a-z]}) ->
        abbreviate(tail, head, result ++ [head])
      true ->
        abbreviate(tail, head, result)
    end
  end
end

Tags:

construct:bitstring
construct:charlist
construct:cond
construct:do-end
construct:doc-string
construct:function
construct:function-overloading
construct:invocation
construct:list
construct:module
construct:parameter
construct:pattern-matching
construct:pipe-operator
construct:string
construct:underscore-variables
construct:visibility-modifiers
paradigm:functional
paradigm:metaprogramming
paradigm:pattern-matching
technique:recursion
uses:String.codepoints
uses:String.match?
ErikSchierboom commented 1 year ago

Exercise: simple-linked-list

Code

defmodule LinkedList do
  defmodule Node do
    defstruct [ data: "", next: nil ]
  end

  @opaque t :: tuple()

  @doc """
  Construct a new LinkedList
  """
  @spec new() :: t
  def new() do
     %{head: self(), size: 0}
  end

  @doc """
  Push an item onto a LinkedList
  """
  @spec push(t, any()) :: t
  def push(list, elem) do
    node = %Node{data: elem, next: list[:head]}
    %{head: node, size: list[:size] +1}
  end

  @doc """
  Calculate the length of a LinkedList
  """
  @spec length(t) :: non_neg_integer()
  def length(list) do
    list[:size]
  end

  @doc """
  Determine if a LinkedList is empty
  """
  @spec empty?(t) :: boolean()
  def empty?(list) do
    list[:size] == 0
  end

  @doc """
  Get the value of a head of the LinkedList
  """
  @spec peek(t) :: {:ok, any()} | {:error, :empty_list}
  def peek(list) do
    node = list.head
    cond do
      is_pid(node) ->
        {:error, :empty_list}
      true ->
        {:ok, node.data}
    end
  end

  @doc """
  Get tail of a LinkedList
  """
  @spec tail(t) :: {:ok, t} | {:error, :empty_list}
  def tail(list) do
    node = list.head
    cond do
      is_pid(node) ->
        {:error, :empty_list}
      true ->
        next_node = node.next
        {:ok, %{head: next_node}}
    end
  end

  @doc """
  Remove the head from a LinkedList
  """
  @spec pop(t) :: {:ok, any(), t} | {:error, :empty_list}
  def pop(list) do
    case tail(list) do
      {:ok, new_tail} ->
        {:ok, item} = peek(list)
        {:ok, item, Map.put(new_tail, :size, list[:size] -1)}
      value ->
        value  # return {:error, :empty_list}
    end

  end

  @doc """
  Construct a LinkedList from a stdlib List
  """
  @spec from_list(list()) :: t
  def from_list(list) do
    Enum.reverse(list)
    |> Enum.reduce(LinkedList.new(), &LinkedList.push(&2,&1))
  end

  @doc """
  Construct a stdlib List LinkedList from a LinkedList
  """
  @spec to_list(t) :: list()
  def to_list(list) do
    go_deep(list.head, [])
  end

  defp go_deep(node, curr) when is_pid(node) do
    Enum.reverse curr
  end

  defp go_deep(node, curr) do
    new_curr = [node.data | curr]
    go_deep(node.next, new_curr)
  end

  @doc """
  Reverse a LinkedList
  """
  @spec reverse(t) :: t
  def reverse(list) do
    to_list(list)
    |> Enum.reverse
    |> from_list
  end
end

Tags:

construct:add
construct:annotation
construct:assignment
construct:boolean
construct:case
construct:constructor
construct:def
construct:defmodule
construct:doc-string
construct:field
construct:function
construct:header
construct:invocation
construct:keyword-list
construct:list
construct:map
construct:named-argument
construct:number
construct:parameter
construct:pattern-matching
construct:pipe
construct:struct
construct:subtract
construct:tuple
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:object-oriented
technique:higher-order-functions
technique:recursion
ErikSchierboom commented 1 year ago

Exercise: anagram

Code

defmodule Anagram do
  @doc """
  Returns all candidates that are anagrams of, but not equal to, 'base'.
  """
  @spec match(String.t, [String.t]) :: [String.t]
  def match(base, candidates) do
    Enum.filter(candidates, &(are_anagrams?(base, &1) && !are_similar?(base, &1)))
  end

  defp are_anagrams?(a, b) do
    fingerprint(a) == fingerprint(b)
  end

  defp fingerprint(str) do
    str |> String.downcase |> String.to_char_list |> fingerprint(%{})
  end

  defp fingerprint(list, acc) do
    case list do
      []    -> acc
      [h|t] -> fingerprint(t, Map.put(acc, h, Map.get(acc, h, 0) + 1))
    end
  end

  defp are_similar?(a, b) do
    String.downcase(a) == String.downcase(b)
  end
end

Tags:

construct:add
construct:ampersand
construct:annotation
construct:case
construct:char-list
construct:constructor
construct:doc-string
construct:invocation
construct:lambda
construct:list
construct:map
construct:module
construct:number
construct:parameter
construct:pattern-matching
construct:recursion
construct:string
construct:underscore
construct:visibility-modifiers
paradigm:functional
paradigm:object-oriented
technique:higher-order-functions
technique:recursion
ErikSchierboom commented 1 year ago

Exercise: triangle

Code

defmodule Triangle do
  @type kind :: :equilateral | :isosceles | :scalene

  @side_count_type_mapping %{ 1 => :equilateral, 2 => :isosceles, 3 => :scalene }

  @doc """
  Return the kind of triangle of a triangle with 'a', 'b' and 'c' as lengths.
  """
  @spec kind(number, number, number) :: { :ok, kind } | { :error, String.t }
  def kind(a, b, c) do
    list = [head|tail] = Enum.reverse Enum.sort [a,b,c]
    cond do
      Enum.any?(list, &(&1 <= 0))           -> { :error, "all side lengths must be positive" }
      Enum.sum(tail) <= head                -> { :error, "side lengths violate triangle inequality" }
      true                                  -> { :ok, @side_count_type_mapping[length(Enum.uniq(list))] }
    end
  end
end

Tags:

construct:assignment
construct:atom
construct:boolean
construct:cond
construct:do-end
construct:documentation
construct:expression
construct:field
construct:guard
construct:invocation
construct:list
construct:map
construct:module
construct:number
construct:pattern-matching
construct:sort
construct:string
construct:struct
construct:term-ordering
construct:tuple
construct:type
construct:type-conversion
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:metaprogramming
paradigm:pattern-matching
technique:sorting
ErikSchierboom commented 1 year ago

Exercise: beer-song

Code

defmodule BeerSong do
  @doc """
  Get a single verse of the beer song
  """
  @spec verse(integer) :: String.t
  def verse(number) do
    "#{first_line number-1}\n#{second_line number-2}\n"
  end

  def lyrics, do: lyrics(100..1)

  @doc """
  Get the entire beer song for a given range of numbers of bottles.
  """
  @spec lyrics(Range.t) :: String.t
  def lyrics(range) do
    range
    |> Enum.map(fn(i) -> verse(i) end)
    |> Enum.join("\n")
  end

  defp first_line(0), do: "No more bottles of beer on the wall, no more bottles of beer."
  defp first_line(1), do: "1 bottle of beer on the wall, 1 bottle of beer."
  defp first_line(number), do: "#{number} bottles of beer on the wall, #{number} bottles of beer."

  defp second_line(-1), do: "Go to the store and buy some more, 99 bottles of beer on the wall."
  defp second_line(0), do: "Take it down and pass it around, no more bottles of beer on the wall."
  defp second_line(1), do: "Take one down and pass it around, 1 bottle of beer on the wall."
  defp second_line(number), do: "Take one down and pass it around, #{number} bottles of beer on the wall."
end

Tags:

construct:string-interpolation
construct:annotation
construct:atom
construct:bitstring
construct:charlist
construct:doc-string
construct:function
construct:function-overloading
construct:integer
construct:invocation
construct:method
construct:module
construct:number
construct:parameter
construct:pattern-matching
construct:pipe
construct:private-function
construct:range
construct:string
construct:subtract
construct:tag
construct:visibility-modifiers
paradigm:functional
paradigm:object-oriented
technique:functions
technique:higher-order-functions
ErikSchierboom commented 1 year ago

Exercise: isogram

Code

defmodule Isogram do
  @doc """
  Determines if a word or sentence is an isogram
  """
  @spec isogram?(String.t()) :: boolean
  def isogram?(sentence) do
    letters =
      sentence
      |> String.replace(~r/\s|-|_/, "")
      |> String.codepoints()

    original = letters                |> Enum.count()
    modified = letters |> Enum.uniq() |> Enum.count()

#    IO.inspect([letters, original, modified])
    original == modified
  end
end

Tags:

construct:comment
construct:defmodule
construct:do
construct:module-attribute
construct:parameter
construct:pipeline
construct:regex
construct:string
construct:underscore
construct:verbatim-string
construct:visibility-modifiers
paradigm:functional
paradigm:regular-expressions
technique:higher-order-functions
uses:Regex
ErikSchierboom commented 1 year ago

Exercise: etl

Code

defmodule ETL do
  @doc """
  Transform an index into an inverted index.

  ## Examples

  iex> ETL.transform(%{"a" => ["ABILITY", "AARDVARK"]}, "b" => ["BALLAST", "BEAUTY"]})
  %{"ability" => "a", "aardvark" => "a", "ballast" => "b", "beauty" =>"b"}
  """
  @spec transform(Dict.t) :: map()
  def transform(input) do
    Enum.reduce input, %{}, fn({old_key, old_values}, map) ->
      Enum.reduce old_values, map, fn(old_value, map2) ->
        Dict.put map2, String.downcase(old_value), old_key
      end
    end
  end
end

Tags:

construct:charlist
construct:definition
construct:doc-string
construct:fn
construct:function
construct:invocation
construct:map
construct:module
construct:parameter
construct:pattern-matching
construct:reduce
construct:string
construct:tuple
construct:visibility-modifiers
paradigm:functional
paradigm:object-oriented
technique:higher-order-functions
ErikSchierboom commented 1 year ago

Exercise: grains

Code

defmodule Grains do
  @doc """
  Calculate two to the power of the input minus one.
  """
  @spec square(pos_integer) :: pos_integer
  def square(1), do: 1
  def square(2), do: 2
  def square(n), do: 2 * square(n - 1)

  @doc """
  Adds square of each number from 1 to 64.
  """
  @spec total :: pos_integer
  def total, do: total(64)
  def total(1), do: 1
  def total(n), do: square(n) + total(n - 1)

end

Tags:

construct:add
construct:annotation
construct:attribute
construct:doc-string
construct:invocation
construct:method
construct:multiply
construct:parameter
construct:recursion
construct:specification
construct:subtract
paradigm:functional
technique:higher-order-functions
technique:math
technique:recursion
ErikSchierboom commented 1 year ago

Exercise: change

Code

defmodule Change do
  @doc """
    Determine the least number of coins to be given to the user such
    that the sum of the coins' value would equal the correct amount of change.
    It returns :error if it is not possible to compute the right amount of coins.
    Otherwise returns the tuple {:ok, map_of_coins}

    ## Examples

      iex> Change.generate(3, [5, 10, 15])
      :error

      iex> Change.generate(18, [1, 5, 10])
      {:ok, %{1 => 3, 5 => 1, 10 => 1}}

  """

  @spec generate(integer, list) :: {:ok, map} | :error
  def generate(amount, values) do
    do_generate(
      amount,
      Enum.sort(values, &(&2 < &1)),
      Enum.into(values, %{ }, &{&1, 0})
    )
  end

  defp do_generate(0, _values, change), do: {:ok, change}
  defp do_generate(amount, values, change) do
    case Enum.find(values, fn value -> value <= amount end) do
      nil ->
        :error
      coin ->
        do_generate(
          amount - coin,
          values,
          Map.update!(change, coin, &(&1 + 1))
        )
    end
  end
end

Tags:

construct:add
construct:assignment
construct:atom
construct:case
construct:docstring
construct:function
construct:invocation
construct:lambda
construct:list
construct:map
construct:module
construct:parameter
construct:pattern-matching
construct:recursion
construct:sort
construct:specification
construct:subtract
construct:tuple
construct:underscore
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:metaprogramming
paradigm:recursive
technique:higher-order-functions
uses:Enum.sort
uses:Map.update!
ErikSchierboom commented 1 year ago

Exercise: scale-generator

Code

defmodule ScaleGenerator do

  @doc """
  Find the note for a given interval (`step`) in a `scale` after the `tonic`.

  "m": one semitone
  "M": two semitones (full tone)
  "A": augmented second (three semitones)

  Given the `tonic` "D" in the `scale` (C C# D D# E F F# G G# A A# B C), you
  should return the following notes for the given `step`:

  "m": D#
  "M": E
  "A": F
  """

  @steps %{ "m" => 1, "M" => 2, "A" => 3 }

  @spec step(scale :: list(String.t()), tonic :: String.t(), step :: String.t()) :: list(String.t())
  def step scale, tonic, step do
    offset = Enum.find_index(scale, &(&1 == tonic)) + @steps[step]
    Enum.at scale, offset
  end

  @doc """
  The chromatic scale is a musical scale with thirteen pitches, each a semitone
  (half-tone) above or below another.

  Notes with a sharp (#) are a semitone higher than the note below them, where
  the next letter note is a full tone except in the case of B and E, which have
  no sharps.

  Generate these notes, starting with the given `tonic` and wrapping back
  around to the note before it, ending with the tonic an octave higher than the
  original. If the `tonic` is lowercase, capitalize it.

  "C" should generate: ~w(C C# D D# E F F# G G# A A# B C)
  """

  @chromatic_scale ~w[ C C# D D# E F F# G G# A A# B ]

  @spec chromatic_scale(tonic :: String.t()) :: list(String.t())
  def chromatic_scale tonic \\ "C" do
    _chromatic_scale @chromatic_scale, tonic
  end

  @doc """
  Sharp notes can also be considered the flat (b) note of the tone above them,
  so the notes can also be represented as:

  A Bb B C Db D Eb E F Gb G Ab

  Generate these notes, starting with the given `tonic` and wrapping back
  around to the note before it, ending with the tonic an octave higher than the
  original. If the `tonic` is lowercase, capitalize it.

  "C" should generate: ~w(C Db D Eb E F Gb G Ab A Bb B C)
  """

  @flat_chromatic_scale ~w[ C Db D Eb E F Gb G Ab A Bb B ]

  @spec flat_chromatic_scale(tonic :: String.t()) :: list(String.t())
  def flat_chromatic_scale tonic \\ "C" do
    _chromatic_scale @flat_chromatic_scale, tonic
  end

  defp _chromatic_scale scale, tonic do
    offset = Enum.find_index scale, &(&1 == String.capitalize(tonic))
    Enum.slice(scale, offset..-1) ++ Enum.take(scale, offset+1)
  end

  @doc """
  Certain scales will require the use of the flat version, depending on the
  `tonic` (key) that begins them, which is C in the above examples.

  For any of the following tonics, use the flat chromatic scale:

  F Bb Eb Ab Db Gb d g c f bb eb

  For all others, use the regular chromatic scale.
  """
  @spec find_chromatic_scale(tonic :: String.t()) :: list(String.t())
  def find_chromatic_scale(tonic) when tonic in ~w[ F Bb Eb Ab Db Gb d g c f bb eb ] do
    flat_chromatic_scale tonic
  end
  def find_chromatic_scale tonic do
    chromatic_scale tonic
  end

  @doc """
  The `pattern` string will let you know how many steps to make for the next
  note in the scale.

  For example, a C Major scale will receive the pattern "MMmMMMm", which
  indicates you will start with C, make a full step over C# to D, another over
  D# to E, then a semitone, stepping from E to F (again, E has no sharp). You
  can follow the rest of the pattern to get:

  C D E F G A B C
  """
  @spec scale(tonic :: String.t(), pattern :: String.t()) :: list(String.t())
  def scale(tonic, pattern) do
    next_step find_chromatic_scale(tonic), pattern
  end

  defp next_step(tonic, ""), do: tonic
  defp next_step(scale, pattern) do
    { step, next_pattern } = String.split_at pattern, 1
    { _,    next_scale   } = Enum.split scale, @steps[step]
    [ List.first(scale) | next_step(next_scale, next_pattern) ]
  end

end

Tags:

construct:add
construct:assignment
construct:atom
construct:bitstring
construct:charlist
construct:doc-comment
construct:enum
construct:field
construct:hexadecimal-integer
construct:indexing
construct:invocation
construct:keyword-argument
construct:list
construct:map
construct:module
construct:number
construct:optional-parameter
construct:pattern-matching
construct:recursion
construct:string
construct:struct
construct:tagged-atom
construct:tuple
construct:underscore
construct:variable
construct:when-clause
paradigm:functional
paradigm:imperative
paradigm:metaprogramming
paradigm:recursive
technique:bit-manipulation
technique:bit-shifting
ErikSchierboom commented 1 year ago

Exercise: gigasecond

Code

defmodule Gigasecond do

    @doc """
    Calculate a date one billion seconds after an input date.
    """
    @spec from({pos_integer, pos_integer, pos_integer}) :: :calendar.date

    def from({year, month, day}) do
    date_to_seconds({year,month,day}) + 1_000_000_000
    |> seconds_to_date
    end

  @spec date_to_seconds({pos_integer, pos_integer, pos_integer}) :: pos_integer
  defp date_to_seconds({year, month, day}) do
    {{year,month,day}, {0,0,0}}
    |> :calendar.datetime_to_gregorian_seconds
  end

  @spec seconds_to_date(number) :: :calendar.date
  defp seconds_to_date(seconds) do
    seconds
    |> :calendar.gregorian_seconds_to_datetime
    |> Tuple.to_list
    |> hd
  end
end

Tags:

construct:add
construct:attribute
construct:date
construct:date_to_seconds
construct:def
construct:defp
construct:doc
construct:hexadecimal
construct:integer
construct:invocation
construct:method
construct:module
construct:number
construct:parameter
construct:pipeline
construct:specification
construct:tuple
construct:underscored_number
construct:variable
construct:visibility-modifiers
paradigm:functional
technique:functional-composition
uses:Tuple
ErikSchierboom commented 1 year ago

Exercise: binary-search-tree

Code

defmodule BinarySearchTree do

  @type bst_node :: %{data: any, left: bst_node | nil, right: bst_node | nil}

  @doc """
  Create a new Binary Search Tree with root's value as the given 'data'
  """
  @spec new(any) :: bst_node
  def new(data), do: %{ data: data, left: nil, right: nil }

  @doc """
  Creates and inserts a node with its value as 'data' into the tree.
  """
  @spec insert(bst_node, any) :: bst_node
  def insert(nil, new_data), do: new(new_data)
  def insert(tree = %{ data: data, left: left, right: _}, new_data) when data >= new_data do
    %{ tree | left: insert(left, new_data) }
  end
  def insert(tree, new_data), do: %{ tree | right: insert(tree[:right], new_data) }

  @doc """
  Traverses the Binary Search Tree in order and returns a list of each node's data.
  """
  @spec in_order(bst_node) :: [any]
  def in_order(nil), do: []
  def in_order(tree), do: in_order(tree[:left]) ++ [ tree[:data] ] ++ in_order(tree[:right])

end

Tags:

construct:assignment
construct:atom
construct:binary
construct:bitstring
construct:boolean
construct:charlist
construct:doc-comment
construct:field-label
construct:head
construct:invocation
construct:keyword-arguments
construct:list
construct:map
construct:module
construct:parameter
construct:pattern-matching
construct:record
construct:string
construct:tail
construct:type
construct:type-alias
construct:underscore
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:object-oriented
technique:bit-manipulation
technique:bit-shifting
technique:bitwise-operations
technique:higher-order-functions
technique:recursion
uses:BinarySearchTree
ErikSchierboom commented 1 year ago

Exercise: prime-factors

Code

defmodule PrimeFactors do
  @spec factors_for(pos_integer) :: [pos_integer]
  def factors_for(number) do
    factors_for(number, 2, [])
  end

  defp factors_for(1, _factor, factors), do: Enum.reverse(factors)

  defp factors_for(number, factor, factors) when number < factor * factor,
    do: Enum.reverse(factors, [number])

  defp factors_for(number, factor, factors) when rem(number, factor) == 0,
    do: factors_for(div(number, factor), factor, [ factor | factors ])

  defp factors_for(number, factor, factors),
    do: factors_for(number, factor + 1, factors)
end

Tags:

construct:add
construct:bitwise-xor
construct:div
construct:head
construct:invocation
construct:lambda
construct:list
construct:parameter
construct:pattern-matching
construct:private-function
construct:recursion
construct:when-clause
paradigm:functional
paradigm:declarative
technique:bit-manipulation
technique:bit-shifting
ErikSchierboom commented 1 year ago

Exercise: robot-simulator

Code

defmodule RobotSimulator do
  @directions [:north, :east, :south, :west]
  @doc """
  Create a Robot Simulator given an initial direction and position.

  Valid directions are: `:north`, `:east`, `:south`, `:west`
  """
  @spec create(direction :: atom, position :: { integer, integer }) :: any
  def create(direction \\ :north, position \\ {0,0})
  def create(direction, _position) when not direction in @directions do
    { :error, "invalid direction" }
  end
  def create(direction, {x,y}) when is_number(x) and is_number(y) do
    %{position: {x,y}, direction: direction}
  end

  def create(_direction, _position), do: { :error, "invalid position" }

  @doc """
  Simulate the robot's movement given a string of instructions.

  Valid instructions are: "R" (turn right), "L", (turn left), and "A" (advance)
  """
  @spec simulate(robot :: any, instructions :: String.t ) :: any
  def simulate(robot, instructions) do
    {next, rest} = String.next_grapheme(instructions)
    dir = RobotSimulator.direction(robot)
    {x,y} = RobotSimulator.position(robot)
    cond do
      rest == "" -> move(dir, {x,y}, next)
      Regex.match?(~r/[RLA]/, next) -> move(dir, {x,y}, next) |> simulate(rest)
      true -> { :error, "invalid instruction" }
    end
  end

  defp move(:north, {x,y}, "A"), do: RobotSimulator.create(:north, {x, y + 1})
  defp move(:east, {x,y}, "A"), do: RobotSimulator.create(:east, {x + 1, y})
  defp move(:south, {x,y}, "A"), do: RobotSimulator.create(:south, {x, y - 1})
  defp move(:west, {x,y}, "A"), do: RobotSimulator.create(:west, {x - 1, y})

  defp move(dir, {x,y}, "R") do
    new_index = Enum.find_index(@directions, &(&1 == dir)) + 1
    RobotSimulator.create(Enum.at(@directions, rem(new_index, 4)), {x,y})
  end

  defp move(dir, {x,y}, "L") do
    new_index = Enum.find_index(@directions, &(&1 == dir)) - 1
    RobotSimulator.create(Enum.at(@directions, rem(new_index, 4)), {x,y})
  end

  @doc """
  Return the robot's direction.

  Valid directions are: `:north`, `:east`, `:south`, `:west`
  """
  @spec direction(robot :: any) :: atom
  def direction(robot), do: robot.direction

  @doc """
  Return the robot's position.
  """
  @spec position(robot :: any) :: { integer, integer }
  def position(robot), do: robot.position
end

Tags:

construct:add
construct:and
construct:atom
construct:assignment
construct:bitstring
construct:boolean
construct:charlist
construct:cond
construct:constructor
construct:doc-string
construct:enum
construct:field
construct:header
construct:hexadecimal-integer
construct:implicit-conversion
construct:index
construct:invocation
construct:keyword-argument
construct:list
construct:map
construct:module
construct:number
construct:optional-parameter
construct:parameter
construct:pattern-matching
construct:pipe
construct:string
construct:struct
construct:subtract
construct:tag
construct:tuples
construct:underscore
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:metaprogramming
paradigm:object-oriented
technique:bit-manipulation
technique:boolean-logic
technique:higher-order-functions
technique:recursion
uses:Regex
ErikSchierboom commented 1 year ago

Exercise: bowling

Code

defmodule Bowling do

  @doc """
    Creates a new game of bowling that can be used to store the results of
    the game
  """

  @spec start() :: any
  def start do
    []
  end

  @doc """
    Records the number of pins knocked down on a single roll. Returns `:ok`
    unless there is something wrong with the given number of pins, in which
    case it returns a helpful message.
  """

  def roll(_, roll) when not (roll in 0..10) do
    { :error, "Pins must have a value from 0 to 10" }
  end

  def roll([{10} | rest], roll) do
    [ {roll}, {10} | rest ]
  end

  def roll([{a} | _], roll) when a + roll > 10 do
    {:error, "Pin count exceeds pins on the lane"}
  end

  def roll([{a} | rest], roll) do
    [ {a,roll} | rest ]
  end

  def roll(game, roll) do
    [ {roll} | game ]
  end

  @doc """
    Returns the score of a given game of bowling if the game is complete.
    If the game isn't complete, it returns a helpful message.
  """

  @game_not_finished { :error, "Score cannot be taken until the end of the game" }
  @too_many_frames { :error, "Invalid game: too many frames" }

  @spec score(any) :: integer | String.t
  def score(game) do
    cond do
      length(game) == 10 -> score(Enum.reverse(game), 0)
      length(game) == 11 ->
        case game do
          [ {u}, {a,b}  | _ ] when a + b == 10 -> score(Enum.reverse([{:extra, {u}} | tl(game)]), 0)
          [ {x,y}, {10} | _ ] -> score(Enum.reverse([{:extra, {x,y}} | tl(game)]), 0)
          [ {_}, {10}   | _ ] -> @game_not_finished
          _ -> @too_many_frames
        end
      length(game) == 12 ->
        case game do
          [ {10}, {10}, {10} | rest ] -> score( Enum.reverse( [{:extra, {10,10}}, {10} | rest ] ), 0 )
          true -> @too_many_frames
        end
      length(game) < 10 -> @game_not_finished
      true -> @too_many_frames
    end
  end

  def nextroll( [] ) do
    @game_not_finished
  end

  def nextroll( [ {:extra, n} | rest ] ) do
    nextroll( [n | rest] )
  end

  def nextroll( [ {n} | rest ] ) do
    {n, rest}
  end

  def nextroll( [ {n,x} | rest ] ) do
    {n, [{x} | rest] }
  end

  def score( [ {:extra, _} | _ ], acc) do
    acc
  end

  def score( [ {a,b} | rest ], acc ) when a + b == 10 do
    case nextroll rest do
      {:error, str} -> {:error, str}
      {x, _}        -> score(rest, acc + 10 + x)
    end
  end

  def score( [ {a,b} | rest ], acc ) do
    score(rest, acc + a + b)
  end

  def score( [ {10} | rest ], acc ) do
    case nextroll rest do
      {:error, str} -> {:error, str}
      {x, rem1}     ->
        case nextroll rem1 do
          {:error, str} -> {:error, str}
          {y, _} -> score(rest, acc + 10 + x + y)
        end
    end
  end

  def score( [], acc ) do
    acc
  end
end

Tags:

construct:add
construct:assignment
construct:atom
construct:case
construct:cond
construct:constructor
construct:doc-comment
construct:head
construct:integer
construct:invocation
construct:length
construct:list
construct:module
construct:parameter
construct:pattern-matching
construct:recursion
construct:specification
construct:string
construct:underscore
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:object-oriented
technique:recursion
ErikSchierboom commented 1 year ago

Exercise: clock

Code

defmodule Clock do
  defstruct hour: 0, minute: 0

  @doc """
  Returns a string representation of a clock:

      iex> Clock.new(8, 9) |> to_string
      "08:09"
  """
  @spec new(integer, integer) :: Clock
  def new(hour, minute) do
    %Clock{} |> add(hour * 60) |> add(minute)
  end

  @doc """
  Adds two clock times:

      iex> Clock.add(10, 0) |> Clock.add(3) |> to_string
      "10:03"
  """
  @spec add(Clock, integer) :: Clock
  def add(%Clock{hour: hour, minute: minute}, add_minute) do
    hours_from_minutes = div(add_minute, 60)
    remaining_minutes = rem(add_minute, 60)

    hours = cond do
      minute + remaining_minutes < 0 -> hours_from_minutes - 1
      minute + remaining_minutes >= 60 -> hours_from_minutes + 1
      true -> hours_from_minutes
    end

    %Clock{
      hour: (hour + hours) |> scale_to(24), 
      minute: (minute + remaining_minutes) |> scale_to(60) 
    }
  end

  defp scale_to(number, max) when is_integer(number) and is_integer(max) and max > 0 do
    number
    |> rem(max)
    |> Kernel.+(max)
    |> rem(max)
  end

  defimpl String.Chars, for: Clock do
    def to_string(%Clock{hour: hour, minute: minute}) do
      [hour, minute]
      |> Enum.map(&(&1 |> Integer.to_string |> String.rjust(2, ?0)))
      |> Enum.join(":")
    end
  end
end

Tags:

construct:add
construct:and
construct:attribute
construct:boolean
construct:charlist
construct:cond
construct:constructor
construct:defimpl
construct:doc-string
construct:divide
construct:field
construct:function
construct:integer
construct:integral-number
construct:invocation
construct:lambda
construct:list
construct:map
construct:module
construct:multiply
construct:number
construct:parameter
construct:pattern-matching
construct:pipe
construct:struct
construct:subtract
construct:tag
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:object-oriented
technique:higher-order-functions
ErikSchierboom commented 1 year ago

Exercise: say

Code

defmodule Say do

  def in_english(0) do
    {:ok, "zero"}
  end

  def in_english(number) when 0 < number and number < 1_000_000_000_000 do
    english = number |> groups_of_3d() |> groups_eng()
    {:ok, english}
  end

  def in_english(_) do
    {:error, "number is out of range"}
  end

  defp groups_of_3d(number) do
    _groups_of_3d(number, [])
  end

  defp _groups_of_3d(0, acc) do
    Enum.reverse(acc)
  end

  defp _groups_of_3d(number, acc) do
    _groups_of_3d(div(number, 1000), [rem(number, 1000) | acc])
  end

  defp groups_eng(groups) do
    groups
    |> Enum.with_index()
    |> Enum.map(&group_eng/1)
    |> Enum.reject(&is_nil/1)
    |> Enum.reverse()
    |> Enum.join(" ")
  end

  defp group_eng({0, _group_index}) do
    nil
  end

  defp group_eng({num_3d, 0}) do
    "#{num_3d_eng(num_3d)}"
  end

  defp group_eng({num_3d, group_index}) do
    "#{num_3d_eng(num_3d)} #{group_name(group_index)}"
  end

  defp group_name(1), do: "thousand"
  defp group_name(2), do: "million"
  defp group_name(3), do: "billion"

  defp num_3d_eng(num_3d) do
    hundreds = div(num_3d, 100)
    num_2d = rem(num_3d, 100)
    case {hundreds, num_2d} do
      {0, _} -> "#{num_2d_eng(num_2d)}"
      {_, 0} -> "#{num_2d_eng(hundreds)} hundred"
      _      -> "#{num_2d_eng(hundreds)} hundred #{num_2d_eng(num_2d)}"
    end
  end

  defp num_2d_eng(0), do: nil
  defp num_2d_eng(1), do: "one"
  defp num_2d_eng(2), do: "two"
  defp num_2d_eng(3), do: "three"
  defp num_2d_eng(4), do: "four"
  defp num_2d_eng(5), do: "five"
  defp num_2d_eng(6), do: "six"
  defp num_2d_eng(7), do: "seven"
  defp num_2d_eng(8), do: "eight"
  defp num_2d_eng(9), do: "nine"
  defp num_2d_eng(10), do: "ten"
  defp num_2d_eng(11), do: "eleven"
  defp num_2d_eng(12), do: "twelve"
  defp num_2d_eng(13), do: "thirteen"
  defp num_2d_eng(14), do: "fourteen"
  defp num_2d_eng(15), do: "fifteen"
  defp num_2d_eng(16), do: "sixteen"
  defp num_2d_eng(17), do: "seventeen"
  defp num_2d_eng(18), do: "eighteen"
  defp num_2d_eng(19), do: "nineteen"
  defp num_2d_eng(20), do: "twenty"
  defp num_2d_eng(30), do: "thirty"
  defp num_2d_eng(40), do: "forty"
  defp num_2d_eng(50), do: "fifty"
  defp num_2d_eng(60), do: "sixty"
  defp num_2d_eng(70), do: "seventy"
  defp num_2d_eng(80), do: "eighty"
  defp num_2d_eng(90), do: "ninty"

  defp num_2d_eng(num_2d) do
    tens = num_2d |> div(10) |> Kernel.*(10) |> num_2d_eng()
    ones = num_2d |> rem(10) |> num_2d_eng()
    if ones, do: "#{tens}-#{ones}", else: tens
  end
end

Tags:

construct:and
construct:assignment
construct:atom
construct:binary
construct:bitstring
construct:case
construct:charlist
construct:divide
construct:do
construct:enum
construct:expression
construct:guard
construct:hexadecimal
construct:if
construct:if
construct:if_unless
construct:implicit_capture
construct:indexing
construct:invocation
construct:lambda
construct:list
construct:map
construct:module
construct:number
construct:parameter
construct:pattern_matching
construct:pipeline
construct:private_function
construct:string
construct:underscore
construct:variable
construct:when_guard
paradigm:functional
paradigm:imperative
paradigm:object-oriented
technique:higher-order-functions
ErikSchierboom commented 1 year ago

Exercise: say

Code

defmodule Say do
  @doc """
  Translate a positive integer into English.
  """

  @basics %{
    1 => "one",
    2 => "two",
    3 => "three",
    4 => "four",
    5 => "five",
    6 => "six",
    7 => "seven",
    8 => "eight",
    9 => "nine",
    10 => "ten",
    11 => "eleven",
    12 => "twelve",
    13 => "thirteen",
    14 => "fourteen",
    15 => "fifteen",
    16 => "sixteen",
    17 => "seventeen",
    18 => "eighteen",
    19 => "nineteen",
    20 => "twenty",
    30 => "thirty",
    40 => "forty",
    50 => "fifty",
    60 => "sixty",
    70 => "seventy",
    80 => "eighty",
    90 => "ninety",
  }

  @spec in_english(integer) :: {atom, String.t}
  def in_english(number) when number < 0 or number > 999_999_999_999, do: {:error, "number is out of range"}
  def in_english(0), do: {:ok, "zero"}
  def in_english(number) do
    {:ok, do_in_english(number, 1_000_000_000, [])}
  end

  def do_in_english(number, 10, acc) do
    acc ++ [two_digits(number)]
    |> Enum.join(" ")
    |> String.trim
  end
  def do_in_english(number, 100, acc) do
    do_in_english(rem(number, 100), 10, acc ++ [hundreds(div(number, 100))])
  end
  def do_in_english(number, 1_000, acc) do
    do_in_english(rem(number, 1_000), 100, acc ++ [thousands(div(number, 1_000))])
  end
  def do_in_english(number, 1_000_000, acc) do
    do_in_english(rem(number, 1_000_000), 1_000, acc ++ [millions(div(number, 1_000_000))])
  end
  def do_in_english(number, 1_000_000_000, acc) do
    do_in_english(rem(number, 1_000_000_000), 1_000_000, acc ++ [billions(div(number, 1_000_000_000))])
  end

  defp two_digits(number) do
    case div(number, 10) do
        n when n in [0, 1] -> Map.get(@basics, number, "")
        n -> Map.get(@basics, n * 10) <> "-" <> Map.get(@basics, rem(number, 10), "") |> String.trim_trailing("-")
    end
  end

  defp hundreds(0), do: ""
  defp hundreds(number) do
    do_in_english(number, 10, []) <> " hundred"
  end

  defp thousands(0), do: ""
  defp thousands(number) do
    do_in_english(number, 100, []) <> " thousand"
  end

  defp millions(0), do: ""
  defp millions(number) do
    do_in_english(number, 100, []) <> " million"
  end

  defp billions(0), do: ""
  defp billions(number) do
    do_in_english(number, 100, []) <> " billion"
  end
end

Tags:

construct:atom
construct:binary
construct:bitstring
construct:boolean
construct:case
construct:charlist
construct:div
construct:do-end
construct:doc-string
construct:double
construct:exponentiation
construct:expression
construct:floating-point-number
construct:function
construct:function-overloading
construct:guard
construct:hexadecimal-integer
construct:implicit-conversion
construct:integer
construct:invocation
construct:list
construct:map
construct:module
construct:multiply
construct:number
construct:parameter
construct:pattern-matching
construct:record
construct:string
construct:underscored-number
construct:variable
construct:visibility-modifiers
paradigm:functional
paradigm:imperative
paradigm:object-oriented
technique:bit-manipulation
technique:bit-shifting
technique:bitwise-operations
technique:higher-order-functions
technique:looping
uses:Map
uses:charlist
uses:recursion
ErikSchierboom commented 1 year ago

Exercise: say

Code

defmodule Say do
  @doc """
  Translate a positive integer into English.
  """
  @spec in_english(integer) :: {atom, String.t}
  def in_english(number) when (number < 0) or (number > 999_999_999_999) do
    {:error, "number is out of range"}
  end

  def in_english(number) do
    translated =
      number
      |> chunk
      |> Enum.map(&to_english(&1))
      |> Enum.join("\s")
    {:ok, translated}
  end

  @spec chunk(integer) :: [integer | {integer, integer}]
  def chunk(number) do
    []
    |> chunk(number)
    |> Enum.reverse
  end

  @spec chunk(list, integer) :: [integer | {integer, integer}]
  def chunk(chunks, number) do
    cond do
      number <= 20 ->
        {number, 0}
      number < 100 ->
        {div(number, 10) * 10, rem(number, 10)}
      number < 1000 ->
        {div(number, 100), 100, rem(number, 100)}
      number < 1_000_000 ->
        {div(number, 1000), 1000, rem(number, 1000)}
      number < 1_000_000_000 ->
        {div(number, 1_000_000), 1_000_000, rem(number, 1_000_000)}
      number < 1_000_000_000_000 ->
        {div(number, 1_000_000_000), 1_000_000_000, rem(number, 1_000_000_000)}
    end
    |> case do
      {n, 0} ->
        [n | chunks]
      {n, remainder} when n < 100 and remainder < 10 ->
        [{n, remainder}| chunks]
      {n, remainder} ->
        [n | chunks]
        |> chunk(remainder)
      {n, base, 0} ->
        chunks
        |>chunk(n)
        |> List.insert_at(0, base)
      {n, base, remainder} ->
        chunks
        |> chunk(n)
        |> List.insert_at(0, base)
        |> chunk(remainder)
    end
  end

  @spec to_english(integer) :: String.t
  def to_english(number) do
    case number do
      0 -> "zero"
      1 -> "one"
      2 -> "two"
      3 -> "three"
      4 -> "four"
      5 -> "five"
      6 -> "six"
      7 -> "seven"
      8 -> "eight"
      9 -> "nine"
      10 -> "ten"
      11 -> "eleven"
      12 -> "twelve"
      13 -> "thirteen"
      14 -> "fourteen"
      15 -> "fifhteen"
      16 -> "sixteen"
      17 -> "seventeen"
      18 -> "eighteen"
      19 -> "nineteen"
      20 -> "twenty"
      30 -> "thirty"
      40 -> "forty"
      50 -> "fifty"
      60 -> "sixty"
      70 -> "seventy"
      80 -> "eighty"
      90 -> "ninety"
      100 -> "hundred"
      1_000 -> "thousand"
      1_000_000 -> "million"
      1_000_000_000 -> "billion"
      1_000_000_000_000 -> "trillion"
      {n, n2} -> to_english(n) <> "-" <> to_english(n2)
    end
  end
end

Tags:

construct:and
construct:atom
construct:binary
construct:bitstring
construct:case
construct:cond
construct:decimal
construct:defmodule
construct:doc
construct:double
construct:end
construct:float
construct:floating_point_number
construct:function
construct:function_clause
construct:guard
construct:integer
construct:integral_number
construct:invocation
construct:list
construct:map
construct:multiply
construct:number
construct:or
construct:parameter
construct:pattern_matching
construct:pipe
construct:spec
construct:string
construct:underscore
construct:variable
construct:visibility
paradigm:functional
paradigm:metaprogramming
technique:higher_order_recursion
technique:recursion
ErikSchierboom commented 1 year ago

Exercise: collatz-conjecture

Code

defmodule CollatzConjecture do

  @doc """
  calc/1 takes an integer and returns the number of steps required to get the
  number to 1 when following the rules:
    - if number is odd, multiply with 3 and add 1
    - if number is even, divide by 2
  """
  @spec calc(number :: pos_integer) :: pos_integer
  def calc(input) when is_integer(input) and input > 0 do
    do_calc(input, 0)
  end
  def calc(_), do: raise FunctionClauseError

  defp do_calc(1, acc), do: acc
  defp do_calc(n, acc) when rem(n, 2) == 0, do: do_calc(div(n, 2), acc + 1)
  defp do_calc(n, acc) when rem(n, 2) == 1, do: do_calc(n * 3 + 1, acc + 1)
end

Tags:

construct:add
construct:and
construct:attribute
construct:doc-string
construct:divide
construct:double
construct:elixir-module
construct:floating-point-number
construct:function
construct:function-overloading
construct:invocation
construct:method-overloading
construct:multiply
construct:number
construct:parameter
construct:pattern-matching
construct:raise
construct:spec
construct:underscore
construct:visibility-modifiers
paradigm:functional
paradigm:object-oriented
technique:exceptions
angelikatyborska commented 1 year ago

Sorry for the late reply, but I was considering and considering, but in the end I do not have the time to do this task.

ErikSchierboom commented 1 year ago

This is an automated comment

Hello :wave: Next week we're going to start using the tagging work people are doing on these. If you've already completed the work, thank you! If you've not, but intend to this week, that's great! If you're not going to get round to doing it, and you've not yet posted a comment letting us know, could you please do so, so that we can find other people to do it. Thanks!