From c268be8369b0b8ebd2d23ea4de86beefa370d28f Mon Sep 17 00:00:00 2001 From: meatball <69751659+meatball133@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:00:44 +0100 Subject: [PATCH 1/2] Add introduction and about sections for hashes --- concepts/hashes/about.md | 17 ++++++++++ concepts/hashes/introduction.md | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 concepts/hashes/about.md create mode 100644 concepts/hashes/introduction.md diff --git a/concepts/hashes/about.md b/concepts/hashes/about.md new file mode 100644 index 0000000000..2d978508b5 --- /dev/null +++ b/concepts/hashes/about.md @@ -0,0 +1,17 @@ +# About + +## Creating hashes + + + +## Indexing + +## Modifying + +## Methods + +has_value? +include? + +keys +values \ No newline at end of file diff --git a/concepts/hashes/introduction.md b/concepts/hashes/introduction.md new file mode 100644 index 0000000000..f48acffebe --- /dev/null +++ b/concepts/hashes/introduction.md @@ -0,0 +1,55 @@ +# About + +[Symbols][symbols] are named identifiers that can be used to refer to a value. +Symbols are created through a symbol literal, which is by prefixing a name with a `:` character, e.g. `:foo`. +They also allow for being written with quotes, e.g. `:"foo"`, which allows, for example, spaces in the name. + +```ruby +:foo # => :foo +:"foo boo" # => :"foo boo" +``` + +Symbols are used in many places in the language, including as keys in hashes, to represent method names and variable names. + +## Identifier + +What makes symbols different from strings is that they are identifiers, and do not represent data or text. +This means that two symbols with the same name are always the same object. + +```ruby +"foo".object_id # => 60 +"foo".object_id # => 80 +:foo.object_id # => 1086748 +:foo.object_id # => 1086748 +``` + +## Modifying Symbols + +Symbols are immutable, which means that they cannot be modified. +This means that when you "modify" a symbol, you are actually creating a new symbol. +There are a few methods that can be used to manipulate symbols, they all return new symbols. +All methods can be found in the [Symbol API][symbols-api]. + +```ruby +:foo.upcase # => :FOO + +:foo.object_id # => 1086748 +:foo.upcase.object_id # => 60 +``` + +The benefit of symbols being immutable is that they are more memory efficient than strings, but also safer to use as identifiers. + +## Conversion + +Symbols can be converted to strings and vice versa. +This can be useful when you want to modify a symbol, or when you want to use a symbol as a string. +To present a string as a symbol, you can use the `String#to_sym` method, and to do the opposite, you can use the `Symbol#to_s` method. +Due to symbols having a limited set of methods, it can be useful to convert a symbol to a string to use string methods on it, if a new symbol is needed. + +```ruby +:foo.to_s # => "foo" +"foo".to_sym # => :foo +``` + +[symbols]: https://www.rubyguides.com/2018/02/ruby-symbols/ +[symbols-api]: https://rubyapi.org/o/symbol From aced6cac5e8bd81cca5709921d84b3d9e25c53a6 Mon Sep 17 00:00:00 2001 From: meatball Date: Fri, 9 Jan 2026 13:05:51 +0100 Subject: [PATCH 2/2] Add concept and exercise --- concepts/hashes/.meta/config.json | 5 + concepts/hashes/about.md | 198 ++++++++++++- concepts/hashes/introduction.md | 183 ++++++++++--- concepts/hashes/links.json | 10 + config.json | 20 +- exercises/concept/gross-store/.docs/hints.md | 25 ++ .../concept/gross-store/.docs/instructions.md | 88 ++++++ .../concept/gross-store/.docs/introduction.md | 176 ++++++++++++ .../concept/gross-store/.meta/config.json | 23 ++ exercises/concept/gross-store/.meta/design.md | 32 +++ .../concept/gross-store/.meta/exemplar.rb | 38 +++ exercises/concept/gross-store/gross_store.rb | 24 ++ .../concept/gross-store/gross_store_test.rb | 259 ++++++++++++++++++ 13 files changed, 1042 insertions(+), 39 deletions(-) create mode 100644 concepts/hashes/.meta/config.json create mode 100644 concepts/hashes/links.json create mode 100644 exercises/concept/gross-store/.docs/hints.md create mode 100644 exercises/concept/gross-store/.docs/instructions.md create mode 100644 exercises/concept/gross-store/.docs/introduction.md create mode 100644 exercises/concept/gross-store/.meta/config.json create mode 100644 exercises/concept/gross-store/.meta/design.md create mode 100644 exercises/concept/gross-store/.meta/exemplar.rb create mode 100644 exercises/concept/gross-store/gross_store.rb create mode 100644 exercises/concept/gross-store/gross_store_test.rb diff --git a/concepts/hashes/.meta/config.json b/concepts/hashes/.meta/config.json new file mode 100644 index 0000000000..36312afa35 --- /dev/null +++ b/concepts/hashes/.meta/config.json @@ -0,0 +1,5 @@ +{ + "blurb": "Hashes are collections of key-value pairs, where each unique key maps to a specific value. They are useful for storing and retrieving data based on keys rather than numerical indices.", + "authors": ["meatball133"], + "contributors": ["kotp"] +} diff --git a/concepts/hashes/about.md b/concepts/hashes/about.md index 2d978508b5..acc21b26fd 100644 --- a/concepts/hashes/about.md +++ b/concepts/hashes/about.md @@ -1,17 +1,203 @@ # About -## Creating hashes +[Hashes][hash] are also known as dictionary or map in other languages, they are a mutable unsorted collection which maps keys to values. +Each key is unique and is used to retrieve the corresponding value. +The keys can be of any data type which is hashable (has a `hash` method), this includes strings, numbers, and most data types and objects in Ruby. +Even though Hashes are unordered collections, [Ruby maintains the insertion order of key-value pairs][entry_order]. +This means that when you iterate over a `Hash`, the pairs will be returned in the order they were added. +However, deleting elements may affect the order of remaining elements. +Hashes behavior of maintaining insertion order was introduced in Ruby [1.9][ruby-1.9]. +## Creating Hashes -## Indexing +You can create a `Hash` using curly braces `{}` with key-value pairs formed as `key => value` and separated by commas: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +``` + +You can also mix and match different types of keys and values: + +```ruby +my_hash = {1 => "one", :two => 2, "three" => [3, "three"]} +``` + +Alternatively if the keys are symbols, you can use a more the newer syntax which was introcued in Ruby 1.9: + +```ruby +my_hash = {name: "Alice", age: 30, city: "New York"} +``` + +You can create an empty `Hash` using the `Hash.new` method: + +```ruby +empty_hash = Hash.new +``` + +## Accessing values + +You can access values in a `Hash` instance using its corresponding keys, the syntax reminds of array indexing, but using the key instead of an index: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash["name"] +# => "Alice" +``` + +If the key does not exist in the `Hash` instance, the `[]` method will return `nil`: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] +# => nil +``` + +If the disired behavior is to not return `nil` for non-existing keys, another way of accessing values is by using the [`fetch`][fetch] method, which allows you provide a default value for non-existing keys. +If the `fetch` method is used without a default value and the key does not exist, it will raise a `KeyError` exception. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.fetch("city", "Unknown") +# => "Unknown" + +my_hash.fetch("city") +# => KeyError: key not found: "city" +``` ## Modifying +You can add or update key-value pairs in a `Hash` instance by assigning a value to a key using the assignment operator `=`. +Assigning a value to an existing key will update the value, while assigning a value to a new key will add a new key-value pair: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] = "New York" +my_hash["age"] = 31 + +my_hash +# => {"name" => "Alice", "age" => 31, "city" => "New York"} +``` + +## Default values + +When fetching a value with `[]` for a key that does not exist in the `Hash` instance, Ruby returns `nil` by default. + +```ruby +my_hash = {"name" => "Alice"} +my_hash["age"] +# => nil +``` + +This might not always be desirable, so you can set a default value for the `Hash` instance using `Hash.new(default_value)`. +See that the default value is returned only for keys that do not exist in the `Hash` instance. + +```ruby +my_hash = Hash.new("unknown") +my_hash["name"] = "Alice" +my_hash["age"] +# => "unknown" + +my_hash["name"] +# => "Alice" +``` + +~~~~exercism/note +Be cautious when using mutable objects (like Arrays or other Hashes) as default values, as they can lead to unexpected behavior. +~~~~ + +## Deleting key-value pairs + +You can delete a key-value pair from a `Hash` instance using the [`delete`][delete] method, which takes the key as an argument: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash.delete("age") +my_hash +# => {"name" => "Alice", "city" => "New York"} +``` + ## Methods -has_value? -include? +There are several useful instance methods available for Hashes in Ruby. +Here are some commonly used ones: + +### `has_value?` and `include?` + +You can check if a `Hash` instance contains a specific value using the [`has_value?`][has_value?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.has_value?(30) +# => true +my_hash.has_value?(25) +# => false +``` + +You can check if a `Hash` instance contains a specific key using the [`include?`][include?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.include?("name") +# => true +my_hash.include?("city") +# => false +``` + +### `keys` and `values` + +You can retrieve all the keys of a `Hash` instance using the [`keys`][keys] method, which returns an array of keys. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.keys +# => ["name", "age"] +``` + +You can retrieve all the values of a `Hash` instance using the [`values`][values] method, which returns an array of values. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.values +# => ["Alice", 30] +``` + +## Iterating over Hashes + +You can iterate over the key-value pairs in a `Hash` instance using the `each_pair` method. +This will give you access to both the key and the value for each pair: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_pair do |key, value| + puts "#{key}: #{value}" +end +# Output: +# name: Alice +# age: 30 +``` + +You can also iterate over just the keys or just the values using the [`each_key`][each_key] or [`each_value`][each_value] methods, respectively: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_key do |key| + puts key +end +# Output: +# name +# age +``` -keys -values \ No newline at end of file +[entry_order]: https://docs.ruby-lang.org/en/master/Hash.html#class-Hash-label-Entry+Order +[each_pair]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_pair +[each_key]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_key +[keys]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-keys +[each_value]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_value +[values]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-values +[has_value?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-has_value-3F +[include?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-include-3F +[hash]: https://docs.ruby-lang.org/en/master/Hash.html +[fetch]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-fetch +[delete]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-delete +[ruby-1.9]: https://ruby-doc.org/3.4/NEWS/NEWS-1_9_1.html diff --git a/concepts/hashes/introduction.md b/concepts/hashes/introduction.md index f48acffebe..dd30523ecb 100644 --- a/concepts/hashes/introduction.md +++ b/concepts/hashes/introduction.md @@ -1,55 +1,176 @@ # About -[Symbols][symbols] are named identifiers that can be used to refer to a value. -Symbols are created through a symbol literal, which is by prefixing a name with a `:` character, e.g. `:foo`. -They also allow for being written with quotes, e.g. `:"foo"`, which allows, for example, spaces in the name. +[Hashes][hash] are also known as dictionary or map in other languages, they are a mutable unsorted collection which maps keys to values. +Each key is unique and is used to retrieve the corresponding value. +The keys can be of any data type which is hashable (has a `hash` method), this includes strings, numbers, and most data types and objects in Ruby. + +Even though Hashes are unordered collections, [Ruby maintains the insertion order of key-value pairs][entry_order]. +This means that when you iterate over a `Hash`, the pairs will be returned in the order they were added. +However, deleting elements may affect the order of remaining elements. +Hashes behavior of maintaining insertion order was introduced in Ruby [1.9][ruby-1.9]. + +## Creating Hashes + +You can create a `Hash` using curly braces `{}` with key-value pairs formed as `key => value` and separated by commas: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +``` + +You can also mix and match different types of keys and values: + +```ruby +my_hash = {1 => "one", :two => 2, "three" => [3, "three"]} +``` + +Alternatively if the keys are symbols, you can use a more the newer syntax which was introcued in Ruby 1.9: + +```ruby +my_hash = {name: "Alice", age: 30, city: "New York"} +``` + +You can create an empty `Hash` using the `Hash.new` method: ```ruby -:foo # => :foo -:"foo boo" # => :"foo boo" +empty_hash = Hash.new ``` -Symbols are used in many places in the language, including as keys in hashes, to represent method names and variable names. +## Accessing values -## Identifier +You can access values in a `Hash` instance using its corresponding keys, the syntax reminds of array indexing, but using the key instead of an index: -What makes symbols different from strings is that they are identifiers, and do not represent data or text. -This means that two symbols with the same name are always the same object. +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash["name"] +# => "Alice" +``` + +If the key does not exist in the `Hash` instance, the `[]` method will return `nil`: ```ruby -"foo".object_id # => 60 -"foo".object_id # => 80 -:foo.object_id # => 1086748 -:foo.object_id # => 1086748 +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] +# => nil ``` -## Modifying Symbols +If the disired behavior is to not return `nil` for non-existing keys, another way of accessing values is by using the [`fetch`][fetch] method, which allows you provide a default value for non-existing keys. +If the `fetch` method is used without a default value and the key does not exist, it will raise a `KeyError` exception. -Symbols are immutable, which means that they cannot be modified. -This means that when you "modify" a symbol, you are actually creating a new symbol. -There are a few methods that can be used to manipulate symbols, they all return new symbols. -All methods can be found in the [Symbol API][symbols-api]. +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.fetch("city", "Unknown") +# => "Unknown" + +my_hash.fetch("city") +# => KeyError: key not found: "city" +``` + +## Modifying + +You can add or update key-value pairs in a `Hash` instance by assigning a value to a key using the assignment operator `=`. +Assigning a value to an existing key will update the value, while assigning a value to a new key will add a new key-value pair: ```ruby -:foo.upcase # => :FOO +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] = "New York" +my_hash["age"] = 31 -:foo.object_id # => 1086748 -:foo.upcase.object_id # => 60 +my_hash +# => {"name" => "Alice", "age" => 31, "city" => "New York"} ``` -The benefit of symbols being immutable is that they are more memory efficient than strings, but also safer to use as identifiers. +## Deleting key-value pairs -## Conversion +You can delete a key-value pair from a `Hash` instance using the [`delete`][delete] method, which takes the key as an argument: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash.delete("age") +my_hash +# => {"name" => "Alice", "city" => "New York"} +``` + +## Methods + +There are several useful instance methods available for Hashes in Ruby. +Here are some commonly used ones: + +### `has_value?` and `include?` + +You can check if a `Hash` instance contains a specific value using the [`has_value?`][has_value?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.has_value?(30) +# => true +my_hash.has_value?(25) +# => false +``` + +You can check if a `Hash` instance contains a specific key using the [`include?`][include?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.include?("name") +# => true +my_hash.include?("city") +# => false +``` + +### `keys` and `values` + +You can retrieve all the keys of a `Hash` instance using the [`keys`][keys] method, which returns an array of keys. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.keys +# => ["name", "age"] +``` + +You can retrieve all the values of a `Hash` instance using the [`values`][values] method, which returns an array of values. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.values +# => ["Alice", 30] +``` + +## Iterating over Hashes + +You can iterate over the key-value pairs in a `Hash` instance using the `each_pair` method. +This will give you access to both the key and the value for each pair: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_pair do |key, value| + puts "#{key}: #{value}" +end +# Output: +# name: Alice +# age: 30 +``` -Symbols can be converted to strings and vice versa. -This can be useful when you want to modify a symbol, or when you want to use a symbol as a string. -To present a string as a symbol, you can use the `String#to_sym` method, and to do the opposite, you can use the `Symbol#to_s` method. -Due to symbols having a limited set of methods, it can be useful to convert a symbol to a string to use string methods on it, if a new symbol is needed. +You can also iterate over just the keys or just the values using the [`each_key`][each_key] or [`each_value`][each_value] methods, respectively: ```ruby -:foo.to_s # => "foo" -"foo".to_sym # => :foo +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_key do |key| + puts key +end +# Output: +# name +# age ``` -[symbols]: https://www.rubyguides.com/2018/02/ruby-symbols/ -[symbols-api]: https://rubyapi.org/o/symbol +[entry_order]: https://docs.ruby-lang.org/en/master/Hash.html#class-Hash-label-Entry+Order +[each_pair]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_pair +[each_key]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_key +[keys]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-keys +[each_value]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_value +[values]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-values +[has_value?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-has_value-3F +[include?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-include-3F +[hash]: https://docs.ruby-lang.org/en/master/Hash.html +[fetch]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-fetch +[delete]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-delete +[ruby-1.9]: https://ruby-doc.org/3.4/NEWS/NEWS-1_9_1.html diff --git a/concepts/hashes/links.json b/concepts/hashes/links.json new file mode 100644 index 0000000000..7125c42ff8 --- /dev/null +++ b/concepts/hashes/links.json @@ -0,0 +1,10 @@ +[ + { + "url": "https://docs.ruby-lang.org/en/4.0/Hash.html", + "description": "Ruby docs: Hash" + }, + { + "url": "https://www.geeksforgeeks.org/ruby/ruby-hash-class/", + "description": "GeeksforGeeks: Ruby Hash Class" + } +] diff --git a/config.json b/config.json index 398f48312f..cc28dfa7fa 100644 --- a/config.json +++ b/config.json @@ -166,6 +166,17 @@ "enumeration" ] }, + { + "slug": "gross-store", + "name": "Gross Store", + "uuid": "8a1dcca6-bccc-4684-8e77-ea3d77b44999", + "concepts": [ + "hashes" + ], + "prerequisites": [ + "enumeration" + ] + }, { "slug": "boutique-inventory-improvements", "name": "Boutique Inventory Improvements", @@ -174,7 +185,7 @@ "ostruct" ], "prerequisites": [ - "advanced-enumeration" + "hashes" ] }, { @@ -185,7 +196,7 @@ "multiple-assignment-and-decomposition" ], "prerequisites": [ - "ostruct" + "hashes" ] }, { @@ -1699,6 +1710,11 @@ "slug": "enumeration", "name": "Enumeration" }, + { + "uuid": "609e43cc-8f7f-4672-9e77-7d90b44be977", + "slug": "hashes", + "name": "Hashes" + }, { "uuid": "ed6e1642-3f85-404f-85fa-6d014662d1e4", "slug": "advanced-enumeration", diff --git a/exercises/concept/gross-store/.docs/hints.md b/exercises/concept/gross-store/.docs/hints.md new file mode 100644 index 0000000000..cf9c207844 --- /dev/null +++ b/exercises/concept/gross-store/.docs/hints.md @@ -0,0 +1,25 @@ +# Hints + +## General + +- The needed methods for working with Hashes are covered in the [Ruby docs about Hashes][hash] + +## 1. Create a new bill + +- To create a new bill, you need to reinitialize the customer, see [Ruby docs about Hash new][hash-new] + +## 2. Add item to the customer bill + +- To check whether the given unit of measurement is correct, you can test your measurement map for a key without retrieving a value, you can use the [`include?`][include?] method, see [Ruby docs about Hash include?][include?] + +## 3. Remove item from the customer bill + +- To check whether the given item is in customer bill, you can test your measurement map for a key without retrieving a value, you can use the [`include?`][include?] method, see [Ruby docs about Hash include?][include?] +- To check whether the given unit of measurement is correct, you can test your measurement map for a key without retrieving a value, you can use the [`include?`][include?] method, see [Ruby docs about Hash include?][include?] + +## 4. Return the number of specific item that is in the customer bill + +- To check whether the given item is in customer bill, you can test your measurement map for a key without retrieving a value, you can use the [`include?`][include?] method, see [Ruby docs about Hash include?][include?] +[include?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-include-3F +[hash-new]: https://docs.ruby-lang.org/en/master/Hash.html#class-Hash-label-Creating+a+Hash +[hash]: https://docs.ruby-lang.org/en/master/Hash.html \ No newline at end of file diff --git a/exercises/concept/gross-store/.docs/instructions.md b/exercises/concept/gross-store/.docs/instructions.md new file mode 100644 index 0000000000..30b807a898 --- /dev/null +++ b/exercises/concept/gross-store/.docs/instructions.md @@ -0,0 +1,88 @@ +# Instructions + +A friend of yours has an old wholesale store called **Gross Store**. +The name comes from the quantity of the item that the store sell: it's all in [gross unit][gross-unit]. +Your friend asked you to implement a point of sale (POS) system for his store. +**First, you want to build a prototype for it.** +**In your prototype, your system will only record the quantity.** +Your friend gave you a list of measurements to help you: + +| Unit | Score | +| ------------------ | ----- | +| quarter_of_a_dozen | 3 | +| half_of_a_dozen | 6 | +| dozen | 12 | +| small_gross | 120 | +| gross | 144 | +| great_gross | 1728 | + +## 1. Create a new customer bill + +You need to implement a method that create a new (empty) bill for the customer. +This should be done in the constructor of the `GrossStore` class. + +```ruby +gross_store = GrossStore.new() +gross_store.bill +# => {} +``` + +## 2. Add an item to the customer bill + +To implement this, you'll need to: + +- Return `false` if the given `unit` is not in the `UNITS` hash. +- Otherwise add the item to the customer `bill`, indexed by the item name, then return `true`. +- If the item is already present in the bill, increase its quantity by the amount that belongs to the provided `unit`. + +Implement the `add_item?` method, which takes two parameters: `item` (String) and `unit` (String). + +```ruby +gross_store = GrossStore.new() +gross_store.add_item?("apple", "dozen") +# => true (since dozen is a valid unit) +gross_store.bill +# => {"apple" => 12} +``` + +## 3. Remove an item from the customer bill + +To implement this, you'll need to: + +- Return `false` if the given item is **not** in the bill +- Return `false` if the given `unit` is not in the `UNITS` hash. +- Return `false` if the new quantity would be less than 0. +- If the new quantity is 0, completely remove the item from the `bill` then return `true`. +- Otherwise, reduce the quantity of the item and return `true`. + +Implement the `remove_item?` method, which takes two parameters: `item` (String) and `unit` (String). + +```ruby +gross_store = GrossStore.new() +gross_store.add_item?("apple", "dozen") +gross_store.remove_item?("apple", "dozen") +# => true +gross_store.bill +# => {} +``` + +## 4. Return the quantity of a specific item that is in the customer bill + +To implement this, you'll need to: + +- Return `0` if the `item` is not in the bill. +- Otherwise, return the quantity of the item in the `bill`. + +Implement the `quantity` method, which takes one parameter: `item` (String). + +```ruby +gross_store = GrossStore.new() +gross_store.add_item?("apple", "dozen") +gross_store.add_item?("carrot", "half_of_a_dozen") +gross_store.quantity("apple") +# => 18 +gross_store.quantity("banana") +# => 0 +``` + +[gross-unit]: https://en.wikipedia.org/wiki/Gross_(unit) diff --git a/exercises/concept/gross-store/.docs/introduction.md b/exercises/concept/gross-store/.docs/introduction.md new file mode 100644 index 0000000000..dd30523ecb --- /dev/null +++ b/exercises/concept/gross-store/.docs/introduction.md @@ -0,0 +1,176 @@ +# About + +[Hashes][hash] are also known as dictionary or map in other languages, they are a mutable unsorted collection which maps keys to values. +Each key is unique and is used to retrieve the corresponding value. +The keys can be of any data type which is hashable (has a `hash` method), this includes strings, numbers, and most data types and objects in Ruby. + +Even though Hashes are unordered collections, [Ruby maintains the insertion order of key-value pairs][entry_order]. +This means that when you iterate over a `Hash`, the pairs will be returned in the order they were added. +However, deleting elements may affect the order of remaining elements. +Hashes behavior of maintaining insertion order was introduced in Ruby [1.9][ruby-1.9]. + +## Creating Hashes + +You can create a `Hash` using curly braces `{}` with key-value pairs formed as `key => value` and separated by commas: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +``` + +You can also mix and match different types of keys and values: + +```ruby +my_hash = {1 => "one", :two => 2, "three" => [3, "three"]} +``` + +Alternatively if the keys are symbols, you can use a more the newer syntax which was introcued in Ruby 1.9: + +```ruby +my_hash = {name: "Alice", age: 30, city: "New York"} +``` + +You can create an empty `Hash` using the `Hash.new` method: + +```ruby +empty_hash = Hash.new +``` + +## Accessing values + +You can access values in a `Hash` instance using its corresponding keys, the syntax reminds of array indexing, but using the key instead of an index: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash["name"] +# => "Alice" +``` + +If the key does not exist in the `Hash` instance, the `[]` method will return `nil`: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] +# => nil +``` + +If the disired behavior is to not return `nil` for non-existing keys, another way of accessing values is by using the [`fetch`][fetch] method, which allows you provide a default value for non-existing keys. +If the `fetch` method is used without a default value and the key does not exist, it will raise a `KeyError` exception. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.fetch("city", "Unknown") +# => "Unknown" + +my_hash.fetch("city") +# => KeyError: key not found: "city" +``` + +## Modifying + +You can add or update key-value pairs in a `Hash` instance by assigning a value to a key using the assignment operator `=`. +Assigning a value to an existing key will update the value, while assigning a value to a new key will add a new key-value pair: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash["city"] = "New York" +my_hash["age"] = 31 + +my_hash +# => {"name" => "Alice", "age" => 31, "city" => "New York"} +``` + +## Deleting key-value pairs + +You can delete a key-value pair from a `Hash` instance using the [`delete`][delete] method, which takes the key as an argument: + +```ruby +my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"} +my_hash.delete("age") +my_hash +# => {"name" => "Alice", "city" => "New York"} +``` + +## Methods + +There are several useful instance methods available for Hashes in Ruby. +Here are some commonly used ones: + +### `has_value?` and `include?` + +You can check if a `Hash` instance contains a specific value using the [`has_value?`][has_value?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.has_value?(30) +# => true +my_hash.has_value?(25) +# => false +``` + +You can check if a `Hash` instance contains a specific key using the [`include?`][include?] method. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.include?("name") +# => true +my_hash.include?("city") +# => false +``` + +### `keys` and `values` + +You can retrieve all the keys of a `Hash` instance using the [`keys`][keys] method, which returns an array of keys. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.keys +# => ["name", "age"] +``` + +You can retrieve all the values of a `Hash` instance using the [`values`][values] method, which returns an array of values. + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.values +# => ["Alice", 30] +``` + +## Iterating over Hashes + +You can iterate over the key-value pairs in a `Hash` instance using the `each_pair` method. +This will give you access to both the key and the value for each pair: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_pair do |key, value| + puts "#{key}: #{value}" +end +# Output: +# name: Alice +# age: 30 +``` + +You can also iterate over just the keys or just the values using the [`each_key`][each_key] or [`each_value`][each_value] methods, respectively: + +```ruby +my_hash = {"name" => "Alice", "age" => 30} +my_hash.each_key do |key| + puts key +end +# Output: +# name +# age +``` + +[entry_order]: https://docs.ruby-lang.org/en/master/Hash.html#class-Hash-label-Entry+Order +[each_pair]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_pair +[each_key]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_key +[keys]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-keys +[each_value]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-each_value +[values]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-values +[has_value?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-has_value-3F +[include?]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-include-3F +[hash]: https://docs.ruby-lang.org/en/master/Hash.html +[fetch]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-fetch +[delete]: https://docs.ruby-lang.org/en/master/Hash.html#method-i-delete +[ruby-1.9]: https://ruby-doc.org/3.4/NEWS/NEWS-1_9_1.html diff --git a/exercises/concept/gross-store/.meta/config.json b/exercises/concept/gross-store/.meta/config.json new file mode 100644 index 0000000000..76a4496374 --- /dev/null +++ b/exercises/concept/gross-store/.meta/config.json @@ -0,0 +1,23 @@ +{ + "authors": [ + "meatball133" + ], + "contributors": [ + "kotp" + ], + "files": { + "solution": [ + "gross_store.rb" + ], + "test": [ + "gross_store_test.rb" + ], + "exemplar": [ + ".meta/exemplar.rb" + ] + }, + "forked_from": [ + "go/gross-store" + ], + "blurb": "Learn about `Hash` by selling items by the dozen at the Gross Store." +} diff --git a/exercises/concept/gross-store/.meta/design.md b/exercises/concept/gross-store/.meta/design.md new file mode 100644 index 0000000000..8fd320e66b --- /dev/null +++ b/exercises/concept/gross-store/.meta/design.md @@ -0,0 +1,32 @@ +# Design + +## Goal + +The goal is to introduce the student to `Hash` as the class, and as a concept. + +## Learning objectives + +- initializing `Hash` instances +- setting key-value pairs +- modifying values for existing keys +- deleting key-value pairs +- reading a key, non-existent key returns zero value of value type + +## Out of scope + +- iterate over Hash + +## Concepts + +- Hashes +## Prerequisites + +- enumerations +- arrays +- strings +- conditionals +- numbers + +## Representer + +## Analyzer diff --git a/exercises/concept/gross-store/.meta/exemplar.rb b/exercises/concept/gross-store/.meta/exemplar.rb new file mode 100644 index 0000000000..3287264efa --- /dev/null +++ b/exercises/concept/gross-store/.meta/exemplar.rb @@ -0,0 +1,38 @@ +class GrossStore + UNITS = {'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144, 'great_gross' => 1728}.freeze + + attr_reader :bill + + # DO NOT MODIFY ANY OF THE CODE ABOVE THIS LINE + + def initialize + # Initialize an empty bill as a hash + @bill = {} + end + + def add_item?(item, quantity) + # Add the specified quantity of the item to the bill + return false unless UNITS.key?(quantity) + @bill[item] ||= 0 + @bill[item] += UNITS[quantity] + true + end + + def remove_item?(item, quantity) + # Remove the specified quantity of the item from the bill + return false unless UNITS.key?(quantity) + return false unless @bill.key?(item) + return false if @bill[item] < UNITS[quantity] + + @bill[item] -= UNITS[quantity] + if @bill[item].zero? + @bill.delete(item) + end + true + end + + def quantity(item) + # Return the quantity of the specified item in the bill + @bill.fetch(item, 0) + end +end diff --git a/exercises/concept/gross-store/gross_store.rb b/exercises/concept/gross-store/gross_store.rb new file mode 100644 index 0000000000..ee77d9f683 --- /dev/null +++ b/exercises/concept/gross-store/gross_store.rb @@ -0,0 +1,24 @@ +class GrossStore + UNITS = {'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144, + 'great_gross' => 1728}.freeze + + attr_reader :bill + + # DO NOT MODIFY ANY OF THE CODE ABOVE THIS LINE + + def initialize + raise 'Please implement the GrossStore#initialize method' + end + + def add_item?(item, quantity) + raise 'Please implement the GrossStore#add_item? method' + end + + def remove_item?(item, quantity) + raise 'Please implement the GrossStore#remove_item? method' + end + + def quantity(item) + raise 'Please implement the GrossStore#quantity method' + end +end diff --git a/exercises/concept/gross-store/gross_store_test.rb b/exercises/concept/gross-store/gross_store_test.rb new file mode 100644 index 0000000000..5332521493 --- /dev/null +++ b/exercises/concept/gross-store/gross_store_test.rb @@ -0,0 +1,259 @@ +require 'minitest/autorun' +require_relative 'gross_store' + +class GrossStoreTest < Minitest::Test + def test_initialize_with_empty_bill + gross_store = GrossStore.new + + assert_empty(gross_store.bill) + end + + def test_no_unit + gross_store = GrossStore.new + result = gross_store.add_item?('pasta', '') + refute result + assert_empty(gross_store.bill) + end + + def test_unknown_unit + gross_store = GrossStore.new + result = gross_store.add_item?('onion', 'quarter') + refute result + assert_empty(gross_store.bill) + end + + def test_another_unknown_unit + gross_store = GrossStore.new + result = gross_store.add_item?('pasta', 'pound') + refute result + assert_empty(gross_store.bill) + end + + def test_add_item_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('banana', 'half_of_a_dozen') + assert result + assert_equal({ 'banana' => 6 }, gross_store.bill) + end + + def test_add_peas_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('peas', 'quarter_of_a_dozen') + assert result + assert_equal({ 'peas' => 3 }, gross_store.bill) + end + + def test_add_chili_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('chili', 'dozen') + assert result + assert_equal({ 'chili' => 12 }, gross_store.bill) + end + + def test_add_cucumber_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('cucumber', 'small_gross') + assert result + assert_equal({ 'cucumber' => 120 }, gross_store.bill) + end + + def test_add_potato_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('potato', 'gross') + assert result + assert_equal({ 'potato' => 144 }, gross_store.bill) + end + + def test_add_zucchini_to_bill + gross_store = GrossStore.new + result = gross_store.add_item?('zucchini', 'great_gross') + assert result + assert_equal({ 'zucchini' => 1728 }, gross_store.bill) + end + + def test_add_multiple_items_to_bill + gross_store = GrossStore.new + gross_store.add_item?('peas', 'quarter_of_a_dozen') + gross_store.add_item?('peas', 'quarter_of_a_dozen') + gross_store.add_item?('tomato', 'half_of_a_dozen') + gross_store.add_item?('tomato', 'quarter_of_a_dozen') + expected_bill = { + 'peas' => 6, + 'tomato' => 9 + } + assert_equal expected_bill, gross_store.bill + end + + def test_remove_item_that_does_not_exist_in_bill + gross_store = GrossStore.new + result = gross_store.remove_item?('papaya', 'gross') + refute result + assert_empty(gross_store.bill) + end + + def test_remove_item_with_invalid_measurement_unit + gross_store = GrossStore.new + gross_store.add_item?('peas', 'quarter_of_a_dozen') + result = gross_store.remove_item?('peas', 'pound') + refute result + assert_equal({ 'peas' => 3 }, gross_store.bill) + end + + def test_remove_item_with_another_invalid_measurement_unit + gross_store = GrossStore.new + gross_store.add_item?('tomato', 'half_of_a_dozen') + result = gross_store.remove_item?('tomato', 'kilogram') + refute result + assert_equal({ 'tomato' => 6 }, gross_store.bill) + end + + def test_remove_item_with_yet_another_invalid_measurement_unit + gross_store = GrossStore.new + gross_store.add_item?('cucumber', 'small_gross') + result = gross_store.remove_item?('cucumber', 'stone') + refute result + assert_equal({ 'cucumber' => 120 }, gross_store.bill) + end + + def test_remove_item_which_exceeds_existing_quantity + gross_store = GrossStore.new + gross_store.add_item?('peas', 'quarter_of_a_dozen') + result = gross_store.remove_item?('peas', 'half_of_a_dozen') + refute result + assert_equal({ 'peas' => 3 }, gross_store.bill) + end + + def test_remove_another_item_which_exceeds_existing_quantity + gross_store = GrossStore.new + gross_store.add_item?('tomato', 'half_of_a_dozen') + result = gross_store.remove_item?('tomato', 'dozen') + refute result + assert_equal({ 'tomato' => 6 }, gross_store.bill) + end + + def test_remove_multiple_items_from_bill + gross_store = GrossStore.new + gross_store.add_item?('chili', 'dozen') + gross_store.add_item?('cucumber', 'small_gross') + gross_store.add_item?('potato', 'gross') + + gross_store.remove_item?('chili', 'small_gross') + gross_store.remove_item?('cucumber', 'gross') + gross_store.remove_item?('potato', 'great_gross') + + expected_bill = { + 'chili' => 12, + 'cucumber' => 120, + 'potato' => 144 + } + assert_equal expected_bill, gross_store.bill + end + + def test_remove_items_to_zero_quantity + gross_store = GrossStore.new + gross_store.add_item?('peas', 'quarter_of_a_dozen') + + result = gross_store.remove_item?('peas', 'quarter_of_a_dozen') + assert result + assert_empty(gross_store.bill) + end + + def test_remove_another_item_to_zero_quantity + gross_store = GrossStore.new + gross_store.add_item?('tomato', 'half_of_a_dozen') + + result = gross_store.remove_item?('tomato', 'half_of_a_dozen') + assert result + assert_empty(gross_store.bill) + end + + def test_remove_multiple_items_to_zero_quantity + gross_store = GrossStore.new + gross_store.add_item?('chili', 'dozen') + gross_store.add_item?('cucumber', 'small_gross') + gross_store.add_item?('potato', 'gross') + gross_store.add_item?('zucchini', 'great_gross') + + gross_store.remove_item?('chili', 'dozen') + gross_store.remove_item?('cucumber', 'small_gross') + gross_store.remove_item?('potato', 'gross') + gross_store.remove_item?('zucchini', 'great_gross') + + assert_empty(gross_store.bill) + end + + def test_remove_item_to_reduce_quantity + gross_store = GrossStore.new + gross_store.add_item?('chili', 'dozen') + + result = gross_store.remove_item?('chili', 'half_of_a_dozen') + assert result + assert_equal({ 'chili' => 6 }, gross_store.bill) + end + + def test_remove_another_item_to_reduce_quantity + gross_store = GrossStore.new + gross_store.add_item?('cucumber', 'small_gross') + + result = gross_store.remove_item?('cucumber', 'dozen') + assert result + assert_equal({ 'cucumber' => 108 }, gross_store.bill) + end + + def test_remove_yet_another_item_to_reduce_quantity + gross_store = GrossStore.new + gross_store.add_item?('zucchini', 'gross') + + result = gross_store.remove_item?('zucchini', 'quarter_of_a_dozen') + assert result + assert_equal({ 'zucchini' => 141 }, gross_store.bill) + end + + def test_get_item_that_does_not_exist_in_bill + gross_store = GrossStore.new + quantity = gross_store.quantity('grape') + assert_equal 0, quantity + end + + def test_get_item + gross_store = GrossStore.new + gross_store.add_item?('peas', 'quarter_of_a_dozen') + quantity = gross_store.quantity('peas') + assert_equal 3, quantity + end + + def test_get_tomato + gross_store = GrossStore.new + gross_store.add_item?('tomato', 'half_of_a_dozen') + quantity = gross_store.quantity('tomato') + assert_equal 6, quantity + end + + def test_get_chili + gross_store = GrossStore.new + gross_store.add_item?('chili', 'dozen') + quantity = gross_store.quantity('chili') + assert_equal 12, quantity + end + + def test_get_cucumber + gross_store = GrossStore.new + gross_store.add_item?('cucumber', 'small_gross') + quantity = gross_store.quantity('cucumber') + assert_equal 120, quantity + end + + def test_get_potato + gross_store = GrossStore.new + gross_store.add_item?('potato', 'gross') + quantity = gross_store.quantity('potato') + assert_equal 144, quantity + end + + def test_get_zucchini + gross_store = GrossStore.new + gross_store.add_item?('zucchini', 'great_gross') + quantity = gross_store.quantity('zucchini') + assert_equal 1728, quantity + end +end