From 69e4a32226bc8b0a83276d5c79db30a1f939c245 Mon Sep 17 00:00:00 2001 From: meatball <69751659+meatball133@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:30:25 +0200 Subject: [PATCH] Add getters and setters concept and exercise (#687) * Start on concept * More progress * Update config and concept text * Fix config * Fix config name * Fix formatting * Fix formatting again * Changes based on feedback * Update config --- concepts/getters-setters/.meta/config.json | 5 + concepts/getters-setters/.meta/template.md | 1 + concepts/getters-setters/about.md | 96 +++++++++++++++++++ concepts/getters-setters/introduction.md | 95 ++++++++++++++++++ concepts/getters-setters/links.json | 6 ++ config.json | 17 ++++ .../concept/weighing-machine/.docs/hints.md | 24 +++++ .../weighing-machine/.docs/instructions.md | 64 +++++++++++++ .../weighing-machine/.docs/introduction.md | 95 ++++++++++++++++++ .../weighing-machine/.meta/config.json | 18 ++++ .../weighing-machine/.meta/src/exemplar.cr | 17 ++++ .../weighing-machine/.meta/template.md | 1 + .../spec/weighing_machine_spec.cr | 96 +++++++++++++++++++ .../weighing-machine/src/weighing_machine.cr | 10 ++ 14 files changed, 545 insertions(+) create mode 100644 concepts/getters-setters/.meta/config.json create mode 100644 concepts/getters-setters/.meta/template.md create mode 100644 concepts/getters-setters/about.md create mode 100644 concepts/getters-setters/introduction.md create mode 100644 concepts/getters-setters/links.json create mode 100644 exercises/concept/weighing-machine/.docs/hints.md create mode 100644 exercises/concept/weighing-machine/.docs/instructions.md create mode 100644 exercises/concept/weighing-machine/.docs/introduction.md create mode 100644 exercises/concept/weighing-machine/.meta/config.json create mode 100644 exercises/concept/weighing-machine/.meta/src/exemplar.cr create mode 100644 exercises/concept/weighing-machine/.meta/template.md create mode 100644 exercises/concept/weighing-machine/spec/weighing_machine_spec.cr create mode 100644 exercises/concept/weighing-machine/src/weighing_machine.cr diff --git a/concepts/getters-setters/.meta/config.json b/concepts/getters-setters/.meta/config.json new file mode 100644 index 00000000..b5cf3f2b --- /dev/null +++ b/concepts/getters-setters/.meta/config.json @@ -0,0 +1,5 @@ +{ + "blurb": "Crystal has macros for defining getters and setters. These are used to access and modify the instance variables of a class.", + "authors": ["meatball133"], + "contributors": ["ryanplusplus"] +} diff --git a/concepts/getters-setters/.meta/template.md b/concepts/getters-setters/.meta/template.md new file mode 100644 index 00000000..577dbc90 --- /dev/null +++ b/concepts/getters-setters/.meta/template.md @@ -0,0 +1 @@ +{% $getters-setters %} diff --git a/concepts/getters-setters/about.md b/concepts/getters-setters/about.md new file mode 100644 index 00000000..1bbdec87 --- /dev/null +++ b/concepts/getters-setters/about.md @@ -0,0 +1,96 @@ +# Getters and setters + +Getters and setters are methods that allow you to read and write the value of an object's property. +Crystal has macros, which makes it easy to define getters and setters for a property. +Macros are a way to generate code at compile time, which will be covered later in the macro concept. + +In Ruby these methods are defined using the `attr_reader`, `attr_writer` and `attr_accessor` methods and are very similar to Crystals implementation on the surface. +Crystal has defined these as `getter`, `setter` and `property` macros. + +## Getters + +Getter is a macro that defines a method that returns the value of an instance variable. +This means that you no longer have to write `@` in front of the instance variable when you want to access it (in methods, excluding initialize); instead, you can call the method that the getter macro has defined. + +The getter macro can accept multiple instance variables by separating them with commas. + +```crystal +# This: +class Person + @nane : String + @age : Int32 + +  def name + @name +  end + +  def age + @age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  getter name, :age +end +``` + +As you can see, using getter reduces the amount of code you write and makes it easier to read. +Also, Ruby and Crystal differ in that Crystal accepts both the variable name and symbol as arguments for the getter macro. +Symbols will be covered later in the symbol concept. + +## Setters + +Setter is a macro that defines a method that sets the value of an instance variable. +This macro isn't that often found and is commonly the `property` macro used instead. +The methods that will be created will look like `name_of_method=`; the `=` is what makes it so the property can be set. + +This method definition is a bit special since the argument the method receives is after the `=` sign. +Several other special methods in Crystal use this syntax, like the `+` method. + +As with the getter macro, when you want to update the value of an instance variable after defining a setter, you can call the method that the setter macro has defined. + +```crystal +# This: +class Person + @name : String + @age : Int32 + +  def name=(name : String) + @name = name +  end + +  def age=(age : Int32) + @age = age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  setter name, :age +end +``` + +## Property + +Property is a macro that defines both a getter and a setter for an instance variable. + +```crystal +class Person + @name : String + @age : Int32 + +  property name, :age +end +``` + +[getters_and_macros]: https://crystal-lang.org/reference/syntax_and_semantics/methods_and_instance_variables.html#getters-and-setters +[getter]: https://crystal-lang.org/api/Object.html#getter%28%2Anames%2C%26block%29-macro +[setter]: https://crystal-lang.org/api/Object.html#setter%28%2Anames%29-macro +[property]: https://crystal-lang.org/api/Object.html#property%28%2Anames%2C%26block%29-macro diff --git a/concepts/getters-setters/introduction.md b/concepts/getters-setters/introduction.md new file mode 100644 index 00000000..adc75f71 --- /dev/null +++ b/concepts/getters-setters/introduction.md @@ -0,0 +1,95 @@ +# Getters and setters + +Getters and setters are methods that allow you to read and write the value of an object's property. +Crystal has macros, which makes it easy to define getters and setters for a property. +Macros are a way to generate code at compile time, which will be covered later in the macro concept. + +In Ruby these methods are defined using the `attr_reader`, `attr_writer` and `attr_accessor` methods and are very similar to Crystals implementation on the surface. +Crystal has defined these as `getter`, `setter` and `property` macros. + +## Getters + +Getter is a macro that defines a method that returns the value of an instance variable. +This means that you no longer have to write `@` in front of the instance variable when you want to access it (in methods, excluding initialize); instead, you can call the method that the getter macro has defined. + +The getter macro can accept multiple instance variables by separating them with commas. + +```crystal +# This: +class Person + @nane : String + @age : Int32 + +  def name + @name +  end + +  def age + @age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  getter name, :age +end +``` + +As you can see, using getter reduces the amount of code you write and makes it easier to read. +Also, Ruby and Crystal differ in that Crystal accepts both the variable name and symbol as arguments for the getter macro. +Symbols will be covered later in the symbol concept. + +## Setters + +Setter is a macro that defines a method that sets the value of an instance variable. +This macro isn't that often found and is commonly the `property` macro used instead. +The methods that will be created will look like `name_of_method=`; the `=` is what makes it so the property can be set. + +This method definition is a bit special since the argument the method receives is after the `=` sign. +Several other special methods in Crystal use this syntax, like the `+` method. + +As with the getter macro, when you want to update the value of an instance variable after defining a setter, you can call the method that the setter macro has defined. + +```crystal +# This: +class Person + @name : String + @age : Int32 + +  def name=(name : String) + @name = name +  end + +  def age=(age : Int32) + @age = age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  setter name, :age +end +``` + +## Property + +Property is a macro that defines both a getter and a setter for an instance variable. + +```crystal +class Person + @name : String + @age : Int32 + +  property name, :age +end +``` + +[getter]: https://crystal-lang.org/api/Object.html#getter%28%2Anames%2C%26block%29-macro +[setter]: https://crystal-lang.org/api/Object.html#setter%28%2Anames%29-macro +[property]: https://crystal-lang.org/api/Object.html#property%28%2Anames%2C%26block%29-macro diff --git a/concepts/getters-setters/links.json b/concepts/getters-setters/links.json new file mode 100644 index 00000000..82eefaab --- /dev/null +++ b/concepts/getters-setters/links.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://crystal-lang.org/reference/syntax_and_semantics/methods_and_instance_variables.html#getters-and-setters", + "description": "Crystal docs: getters and setters" + } +] diff --git a/config.json b/config.json index aaa4b6d0..c36f242b 100644 --- a/config.json +++ b/config.json @@ -105,6 +105,18 @@ ], "status": "beta" }, + { + "slug": "weighing-machine", + "name": "Weighing Machine", + "uuid": "317930f4-4c97-4962-a04e-76e6d60b7fba", + "concepts": [ + "getters-setters" + ], + "prerequisites": [ + "strings" + ], + "status": "beta" + }, { "slug": "high-school-sweetheart", "name": "High School Sweetheart", @@ -1524,6 +1536,11 @@ "slug": "strings", "name": "Strings" }, + { + "uuid": "b5f40f9a-d3bf-4e82-9918-a04649f08655", + "slug": "getters-setters", + "name": "Getters and Setters" + }, { "uuid": "4dd28cdc-f197-490b-bbfb-5cd33bbdb6af", "slug": "binary-octal-hexadecimal", diff --git a/exercises/concept/weighing-machine/.docs/hints.md b/exercises/concept/weighing-machine/.docs/hints.md new file mode 100644 index 00000000..ee3362cc --- /dev/null +++ b/exercises/concept/weighing-machine/.docs/hints.md @@ -0,0 +1,24 @@ +# Hints + +## General + +- To create the getter and setter methods you should use the `getter`, `setter` and `property` macros. + +## 1. Create an initial state for the weighing machine + +- To initialize the weighing machine you should use the `initialize` method. +- The method should take two arguments, `precision` and `metric`, which should be an `Int32` and `Bool`, respectively. + These should be used to set the instance variables `@precision` and `@metric`. +- The instance variable `@weight` should be set to `0.0`. + +## 2. Allow the weighing machine to have a precision + +- The `getter` macro allows you to define a method that returns the value of an instance variable. + +## 3. Allow the weight to be set on the weighing machine + +- The `property` macro allows you to define a method that gets and sets the value of an instance variable. + +## 4. Allow the machine to be switch between metric and imperial units + +- The `setter` macro allows you to define a method that sets the value of an instance variable. diff --git a/exercises/concept/weighing-machine/.docs/instructions.md b/exercises/concept/weighing-machine/.docs/instructions.md new file mode 100644 index 00000000..49626ab7 --- /dev/null +++ b/exercises/concept/weighing-machine/.docs/instructions.md @@ -0,0 +1,64 @@ +# Instructions + +In this exercise you'll be modeling a weighing machine. + +## 1. Create an initial state for the weighing machine + +When initialized, the weighing machine should refer to its factory settings which is different for where the machine is sold. +Thereby the machine should be able to be initialized with a precision and if it is metric or imperial. + +Implement the `WeighingMachine#initialize` method which takes two arguments, `precision` which is an `Int32` and `metric` which is a `Bool`. +The `metric` argument when `true` means that the machine should use the metric system, otherwise it should use the imperial system. +The initialize method set should also set the instance variable `@weight` to `0.0`. + +```crystal +precision = 3 +metric = true +vm = WeighingMachine.new(precision, metric) +``` + +## 2. Allow the weighing machine to have a precision + +To cater to different demands, we allow each weighing machine to be customized with a precision (the number of digits after the decimal separator). + +Implement the `WeighingMachine#precision` getter method to return the precision of the weighing machine. + +```crystal +precision = 3 +metric = true +vm = WeighingMachine.new(precision, metric) +vm.precision +# => 3 +``` + +## 3. Allow the weight to be set on the weighing machine + +Implement the `WeighingMachine#weight` property to allow the weight to be get _and_ set: + +```crystal +precision = 3 +metric = true +wm = WeighingMachine.new(precision, metric) +wm.weight = 60.5 +wm.weight +# => 60.5 + +wm.weigh +# => 60.5 +``` + +## 4. Allow the machine to be switch between metric and imperial units + +Implement the `WeighingMachine#metric=` property to allow the unit to be set. +It should accept a boolean value. + +```crystal +precision = 3 +metric = true +wm = WeighingMachine.new(precision, metric) +vm.weight = 60.5 +wm.metric = false + +vm.weigh +# => 133.377 +``` diff --git a/exercises/concept/weighing-machine/.docs/introduction.md b/exercises/concept/weighing-machine/.docs/introduction.md new file mode 100644 index 00000000..adc75f71 --- /dev/null +++ b/exercises/concept/weighing-machine/.docs/introduction.md @@ -0,0 +1,95 @@ +# Getters and setters + +Getters and setters are methods that allow you to read and write the value of an object's property. +Crystal has macros, which makes it easy to define getters and setters for a property. +Macros are a way to generate code at compile time, which will be covered later in the macro concept. + +In Ruby these methods are defined using the `attr_reader`, `attr_writer` and `attr_accessor` methods and are very similar to Crystals implementation on the surface. +Crystal has defined these as `getter`, `setter` and `property` macros. + +## Getters + +Getter is a macro that defines a method that returns the value of an instance variable. +This means that you no longer have to write `@` in front of the instance variable when you want to access it (in methods, excluding initialize); instead, you can call the method that the getter macro has defined. + +The getter macro can accept multiple instance variables by separating them with commas. + +```crystal +# This: +class Person + @nane : String + @age : Int32 + +  def name + @name +  end + +  def age + @age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  getter name, :age +end +``` + +As you can see, using getter reduces the amount of code you write and makes it easier to read. +Also, Ruby and Crystal differ in that Crystal accepts both the variable name and symbol as arguments for the getter macro. +Symbols will be covered later in the symbol concept. + +## Setters + +Setter is a macro that defines a method that sets the value of an instance variable. +This macro isn't that often found and is commonly the `property` macro used instead. +The methods that will be created will look like `name_of_method=`; the `=` is what makes it so the property can be set. + +This method definition is a bit special since the argument the method receives is after the `=` sign. +Several other special methods in Crystal use this syntax, like the `+` method. + +As with the getter macro, when you want to update the value of an instance variable after defining a setter, you can call the method that the setter macro has defined. + +```crystal +# This: +class Person + @name : String + @age : Int32 + +  def name=(name : String) + @name = name +  end + +  def age=(age : Int32) + @age = age +  end +end + +# Is the same as this: +class Person + @name : String + @age : Int32 + +  setter name, :age +end +``` + +## Property + +Property is a macro that defines both a getter and a setter for an instance variable. + +```crystal +class Person + @name : String + @age : Int32 + +  property name, :age +end +``` + +[getter]: https://crystal-lang.org/api/Object.html#getter%28%2Anames%2C%26block%29-macro +[setter]: https://crystal-lang.org/api/Object.html#setter%28%2Anames%29-macro +[property]: https://crystal-lang.org/api/Object.html#property%28%2Anames%2C%26block%29-macro diff --git a/exercises/concept/weighing-machine/.meta/config.json b/exercises/concept/weighing-machine/.meta/config.json new file mode 100644 index 00000000..cfc76eed --- /dev/null +++ b/exercises/concept/weighing-machine/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": ["meatball133"], + "contributors": ["ryanplusplus"], + "files": { + "solution": [ + "src/weighing_machine.cr" + ], + "test": [ + "spec/weighing_machine_spec.cr" + ], + "exemplar": [ + ".meta/src/exemplar.cr" + ] + }, + "blurb": "Learn about getter and setters by implementing a weighing machine.", + "icon": "weighing-machine", + "forked_from": ["csharp/weighing-machine"] +} diff --git a/exercises/concept/weighing-machine/.meta/src/exemplar.cr b/exercises/concept/weighing-machine/.meta/src/exemplar.cr new file mode 100644 index 00000000..7ebe42ec --- /dev/null +++ b/exercises/concept/weighing-machine/.meta/src/exemplar.cr @@ -0,0 +1,17 @@ +class WeighingMachine + getter precision : Int32 + setter metric : Bool + property weight : Float64 = 0.0 + + def initialize(precision : Int32, metric : Bool = true) + @precision = precision + @metric = metric + end + + # DO NOT MODIFY ANYTHING BELOW THIS LINE + def weigh : String + weight = @metric ? @weight : @weight * 2.20462 + weight = weight.round(@precision) + weight.to_s + end +end diff --git a/exercises/concept/weighing-machine/.meta/template.md b/exercises/concept/weighing-machine/.meta/template.md new file mode 100644 index 00000000..577dbc90 --- /dev/null +++ b/exercises/concept/weighing-machine/.meta/template.md @@ -0,0 +1 @@ +{% $getters-setters %} diff --git a/exercises/concept/weighing-machine/spec/weighing_machine_spec.cr b/exercises/concept/weighing-machine/spec/weighing_machine_spec.cr new file mode 100644 index 00000000..c4b602c8 --- /dev/null +++ b/exercises/concept/weighing-machine/spec/weighing_machine_spec.cr @@ -0,0 +1,96 @@ +require "spec" +require "../src/*" + +describe WeighingMachine do + describe "initialize" do + it "should initialize the weighing machine with precision 3" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.@precision.should eq(3) + weighing_machine.@metric.should be_true + weighing_machine.@weight.should eq(0.0) + end + + it "should initialize the weighing machine with precision 5 and metric be true" do + weighing_machine = WeighingMachine.new(5, true) + weighing_machine.@precision.should eq(5) + weighing_machine.@metric.should be_true + weighing_machine.@weight.should eq(0.0) + end + + it "should initialize the weighing machine with precision 3 and metric false" do + weighing_machine = WeighingMachine.new(3, false) + weighing_machine.@precision.should eq(3) + weighing_machine.@metric.should be_false + weighing_machine.@weight.should eq(0.0) + end + + it "should allow the weigh method to be called" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.weigh.should eq("0.0") + end + end + + describe "precision" do + it "should return the precision of the weighing machine" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.precision.should eq(3) + end + + it "should return the precision of the weighing machine when precsion is 5" do + weighing_machine = WeighingMachine.new(5, true) + weighing_machine.precision.should eq(5) + end + + it "should not allow modification" do + {% if WeighingMachine.has_method? "precision=" %} + raise "Error: precision should not have a setter" + {% end %} + end + end + + describe "weight" do + it "should return the weight of the weighing machine" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.weight.should eq(0.0) + end + + it "should return the weight of the weighing machine when weight is 5.0" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.weight = 5.0 + weighing_machine.weight.should eq(5.0) + end + + it "should work to call weigh method" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.weight = 5.0 + weighing_machine.weigh.should eq("5.0") + end + end + + describe "metric" do + it "should be able to change to imperial" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.metric = false + weighing_machine.@metric.should eq(false) + end + + it "should allow the initial to be imperial and then change to metric" do + weighing_machine = WeighingMachine.new(3, false) + weighing_machine.metric = true + weighing_machine.@metric.should eq(true) + end + + it "should affect the weigh method" do + weighing_machine = WeighingMachine.new(3, true) + weighing_machine.weight = 5.0 + weighing_machine.metric = false + weighing_machine.weigh.should eq("11.023") + end + + it "should not allow getting" do + {% if WeighingMachine.has_method? "metric" %} + raise "Error: metric should not have a getter" + {% end %} + end + end +end diff --git a/exercises/concept/weighing-machine/src/weighing_machine.cr b/exercises/concept/weighing-machine/src/weighing_machine.cr new file mode 100644 index 00000000..13f94d68 --- /dev/null +++ b/exercises/concept/weighing-machine/src/weighing_machine.cr @@ -0,0 +1,10 @@ +class WeighingMachine + # Write your code here + + # DO NOT MODIFY ANYTHING BELOW THIS LINE + def weigh : String + weight = @metric ? @weight : @weight * 2.20462 + weight = weight.round(@precision) + weight.to_s + end +end