diff --git a/AGENTS.md b/AGENTS.md index 2dd94cf..94b9d77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,18 @@ ## Ruby Primer Agent Instructions +### Installation + +- **Prerequisites**: Ruby 3.2+, pandoc, xelatex, imagemagick, viu, IBM Plex Serif Light font. +- **Setup**: Run `./install.sh` to install Ruby gems, utilities, and configure the app. + ### Build, Lint, and Test +- **Ruby Version**: Requires Ruby 3.2 or higher. - **Testing**: This project uses RSpec. - Run all tests: `bundle exec rspec` - Run a single test file: `bundle exec rspec examples_spec/0.0.2.rb` - **Linting**: There is no linting configuration. Please adhere to the style of existing code. +- **CI**: GitHub Actions run Ruby and JSON syntax checks on pull requests (`.github/workflows/ruby-syntax.yml` and `json-syntax.yml`). Checks run on all PRs and validate `.rb` files, `rp`, and `.json` files using `ruby -c` and `jq`. ### Code Style @@ -13,7 +20,7 @@ - **Naming**: Use `snake_case` for variables and methods. - **Strings**: Use double quotes for strings, especially with interpolation. Use single quotes for `require` statements. - **Methods**: Use parentheses for method definitions with arguments. -- **Error Handling**: Not specified. - **Imports**: Use `require`. - **Types**: Not specified. +- **Terminal Support**: Check for minimum 135 columns x 40 rows at startup. Detect Kitty or iTerm2 for image display; generate PDFs/PNGs for formatted text. - **General**: Follow existing patterns in the code. Explicit `return` is preferred. diff --git a/examples/4.0.0.rb b/examples/4.0.0.rb new file mode 100644 index 0000000..1780989 --- /dev/null +++ b/examples/4.0.0.rb @@ -0,0 +1 @@ +restaurant_menu = # Fill the hash here. diff --git a/examples/4.0.1.rb b/examples/4.0.1.rb new file mode 100644 index 0000000..d5e7bdd --- /dev/null +++ b/examples/4.0.1.rb @@ -0,0 +1,6 @@ +restaurant_menu = { + "Ramen" => 3, + "Dal Makhani" => 4, + "Tea" => 2 +} +## how do you use restaurant_menu to get the price of Ramen? diff --git a/examples/4.0.2.rb b/examples/4.0.2.rb new file mode 100644 index 0000000..cc8f2fc --- /dev/null +++ b/examples/4.0.2.rb @@ -0,0 +1 @@ +restaurant_menu = {} diff --git a/examples/4.1.0.rb b/examples/4.1.0.rb new file mode 100644 index 0000000..06606c0 --- /dev/null +++ b/examples/4.1.0.rb @@ -0,0 +1,5 @@ +restaurant_menu = {"Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2} +restaurant_menu.each do |item, price| + puts "#{item}: $#{price}" +end +restaurant_menu \ No newline at end of file diff --git a/examples/4.1.1.rb b/examples/4.1.1.rb new file mode 100644 index 0000000..4878885 --- /dev/null +++ b/examples/4.1.1.rb @@ -0,0 +1 @@ +restaurant_menu = {"Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2} diff --git a/examples/4.1.2.rb b/examples/4.1.2.rb new file mode 100644 index 0000000..4878885 --- /dev/null +++ b/examples/4.1.2.rb @@ -0,0 +1 @@ +restaurant_menu = {"Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2} diff --git a/examples/4.1.3.rb b/examples/4.1.3.rb new file mode 100644 index 0000000..a762ec3 --- /dev/null +++ b/examples/4.1.3.rb @@ -0,0 +1,9 @@ +normal = Hash.new +was_not_there = normal[:zig] +puts "Wasn't there:" +p was_not_there + +usually_brown = Hash.new("brown") +pretending_to_be_there = usually_brown[:zig] +puts "Pretending to be there:" +p pretending_to_be_there diff --git a/examples/4.1.4.rb b/examples/4.1.4.rb new file mode 100644 index 0000000..6884546 --- /dev/null +++ b/examples/4.1.4.rb @@ -0,0 +1,2 @@ +chuck_norris = Hash[:punch, 99, :kick, 98, :stops_bullets_with_hands, true] +p chuck_norris diff --git a/examples/4.1.5.rb b/examples/4.1.5.rb new file mode 100644 index 0000000..ce63324 --- /dev/null +++ b/examples/4.1.5.rb @@ -0,0 +1,9 @@ +def artax + a = [:punch, 0] + b = [:kick, 72] + c = [:stops_bullets_with_hands, false] + key_value_pairs = # you can do this. you are a champion. + # unlike Artax, who gave up in a swamp. + Hash[key_value_pairs] +end +artax diff --git a/examples_spec/4.0.0.rb b/examples_spec/4.0.0.rb new file mode 100644 index 0000000..c094243 --- /dev/null +++ b/examples_spec/4.0.0.rb @@ -0,0 +1,14 @@ +require 'rspec' + +describe "example code" do + let(:code) { File.read('__TMPFILE__') } + let(:result) { eval(code) } + + it "should return a hash" do + expect(result).to be_a(Hash) + end + + it "should have correct keys and values" do + expect(result).to eq({"Ramen" => 3, "Dal Makhani" => 4, "Tea" => 2}) + end +end \ No newline at end of file diff --git a/examples_spec/4.0.1.rb b/examples_spec/4.0.1.rb new file mode 100644 index 0000000..a0a244e --- /dev/null +++ b/examples_spec/4.0.1.rb @@ -0,0 +1,10 @@ +require 'rspec' + +describe "example code" do + let(:code) { File.read('__TMPFILE__') } + let(:result) { eval(code) } + + it "should return 3" do + expect(result).to eq(3) + end +end \ No newline at end of file diff --git a/examples_spec/4.0.2.rb b/examples_spec/4.0.2.rb new file mode 100644 index 0000000..305191c --- /dev/null +++ b/examples_spec/4.0.2.rb @@ -0,0 +1,13 @@ +require 'rspec' + +describe "example code" do + let(:code) { File.read('__TMPFILE__') } + let(:context) { binding() } + + before { eval(code, context) } + + it "should set restaurant_menu correctly" do + expect(eval("restaurant_menu", context)).to be_a(Hash) + expect(eval("restaurant_menu", context)).to eq({"Dal Makhani" => 4.5, "Tea" => 2}) + end +end \ No newline at end of file diff --git a/examples_spec/4.1.1.rb b/examples_spec/4.1.1.rb new file mode 100644 index 0000000..b03262f --- /dev/null +++ b/examples_spec/4.1.1.rb @@ -0,0 +1,14 @@ +require 'rspec' + +describe "example code" do + let(:code) { File.read('__TMPFILE__') } + let(:result) { eval(code) } + + it "should return a hash" do + expect(result).to be_a(Hash) + end + + it "should have increased prices" do + expect(result).to eq({"Ramen" => 3.3, "Dal Makhani" => 4.4, "Coffee" => 2.2}) + end +end \ No newline at end of file diff --git a/examples_spec/3.1.5.rb b/examples_spec/4.1.2.rb similarity index 66% rename from examples_spec/3.1.5.rb rename to examples_spec/4.1.2.rb index 591069c..55f5e73 100644 --- a/examples_spec/3.1.5.rb +++ b/examples_spec/4.1.2.rb @@ -8,7 +8,7 @@ expect(result).to be_a(Array) end - it "should equal [4, 5, 6, 7]" do - expect(result).to eq([4, 5, 6, 7]) + it "should contain the keys" do + expect(result.sort).to eq(["Coffee", "Dal Makhani", "Ramen"]) end end \ No newline at end of file diff --git a/examples_spec/4.1.5.rb b/examples_spec/4.1.5.rb new file mode 100644 index 0000000..018a977 --- /dev/null +++ b/examples_spec/4.1.5.rb @@ -0,0 +1,14 @@ +require 'rspec' + +describe "example code" do + let(:code) { File.read('__TMPFILE__') } + let(:result) { eval(code) } + + it "should return a hash" do + expect(result).to be_a(Hash) + end + + it "should have correct key-value pairs" do + expect(result).to eq({:punch => 0, :kick => 72, :stops_bullets_with_hands => false}) + end +end \ No newline at end of file diff --git a/lessons/4.0.json b/lessons/4.0.json new file mode 100644 index 0000000..084b820 --- /dev/null +++ b/lessons/4.0.json @@ -0,0 +1,85 @@ +{ + "number": "4.0", + "name": "Introduction to Ruby Hashes", + "subsections": [ + { + "name": "Creating A Hash", + "items": [ + { + "type": "paragraph", + "value": "A Hash is a collection of key-value pairs. You retrieve or create a new entry in a Hash by referring to its key. Hashes are also called 'associative arrays', 'dictionary', 'HashMap' etc. in other languages." + }, + { + "type": "paragraph", + "value": "A blank hash can be declared using two curly braces -- `{}`. Here is an example of a Hash in Ruby:" + }, + { + "type": "paragraph", + "value": "`student_ages = {`\n\n` \"Jack\" => 10,`\n\n` \"Jill\" => 12,`\n\n` \"Bob\" => 14`\n\n`}`" + }, + { + "type": "paragraph", + "value": "The names (`Jack`, `Jill`, `Bob`) are the 'keys', and `10`, `12` and `14` are the corresponding values. Now try creating a simple hash with the following keys and values:" + }, + { + "type": "paragraph", + "value": "`Ramen = 3`\n\n`Dal Makhani = 4`\n\n`Tea = 2`" + }, + { + "type": "test_example", + "value": "4.0.0" + } + ] + }, + { + "name": "Fetch Values from A Hash", + "items": [ + { + "type": "paragraph", + "value": "You can retrieve values from a Hash object using the `[]` operator. The key of the required value should be enclosed within these square brackets." + }, + { + "type": "paragraph", + "value": "Now try finding the price of a Ramen from the `restaurant_menu` hash: write the name of the object, follow it with a square bracket, and place the key inside the brackets. In this case the key is a string so enclose the key in quotes." + }, + { + "type": "test_example", + "value": "4.0.1" + }, + { + "type": "paragraph", + "value": "That was very simple, wasn't it? One small step at a time!" + } + ] + }, + { + "name": "Modifying A Hash", + "items": [ + { + "type": "paragraph", + "value": "Once you've created a `Hash` object, you would want to add new key-value pairs as well as modify existing values." + }, + { + "type": "paragraph", + "value": "Here is how you would set the price of the `\"Ramen\"` key in the `restaurant_menu` hash:" + }, + { + "type": "paragraph", + "value": "`restaurant_menu[\"Ramen\"] = 3`" + }, + { + "type": "paragraph", + "value": "In fact, you can create a blank hash and add all the values later. Now why don't you try assigning the following values to an empty hash?" + }, + { + "type": "paragraph", + "value": "`Dal Makhani = 4.5`\n\n`Tea = 2`" + }, + { + "type": "test_example", + "value": "4.0.2" + } + ] + } + ] +} diff --git a/lessons/4.1.json b/lessons/4.1.json new file mode 100644 index 0000000..52382bb --- /dev/null +++ b/lessons/4.1.json @@ -0,0 +1,93 @@ +{ + "number": "4.1", + "name": "Hashes, In And Out", + "subsections": [ + { + "name": "Iterating over A Hash", + "items": [ + { + "type": "paragraph", + "value": "You can use the `each` method to iterate over all the elements in a Hash. However unlike `Array#each`, when you iterate over a Hash using `each`, it passes two values to the block: the key and the value of each element." + }, + { + "type": "paragraph", + "value": "Let us see how we can use the `each` method to display the restaurant menu." + }, + { + "type": "example", + "value": "4.1.0" + }, + { + "type": "paragraph", + "value": "The restaurant is doing well, but it is forced to raise prices due to increasing costs. Use the `each` method to increase the price of all the items in the `restaurant_menu` by 10%." + }, + { + "type": "paragraph", + "value": "Remember: in the previous example we only displayed the keys and values of each item in the hash. But in this exercise, you have to modify the hash and increase the value of each item." + }, + { + "type": "test_example", + "value": "4.1.1" + }, + { + "type": "paragraph", + "value": "Ideally, any transformation of a collection (like in the example above) should produce a new collection with the original unchanged making the code easier to understand and manage." + }, + { + "type": "paragraph", + "value": "However, speed and memory considerations often (and usually wrongly) trump maintainability and so the approach above is used quite frequently." + } + ] + }, + { + "name": "Extracting The Keys And Values from A Hash", + "items": [ + { + "type": "paragraph", + "value": "Every Hash object has two methods: `keys` and `values`. The `keys` method returns an array of all the keys in the `Hash`. Similarly `values` returns an array of just the values." + }, + { + "type": "paragraph", + "value": "Try getting an array of all the keys in the `restaurant_menu` hash:" + }, + { + "type": "test_example", + "value": "4.1.2" + } + ] + }, + { + "name": "Newer, Faster", + "items": [ + { + "type": "paragraph", + "value": "There are some little-known shortcuts for creating new hashes. They all provide a slightly different convenience. The latter two generate a hash directly from pre-existing key-value pairs. The first simply sets a default value for all elements in the hash. Let's take a look at that first." + }, + { + "type": "example", + "value": "4.1.3" + }, + { + "type": "paragraph", + "value": "As you can see, where a \"normal\" hash always returns `nil` by default, specifying a default in the `Hash` constructor will always return your custom default for any failed lookups on that hash instance." + }, + { + "type": "paragraph", + "value": "The other two shortcuts actually use the `Hash` class's convenience method: `Hash::[]`. They're fairly straightforward. The first takes a flat list of parameters, arranged in pairs. The second takes just one parameter: an array containing arrays which are themselves key-value pairs. Where! That's a mouthful. Let's try the first form:" + }, + { + "type": "example", + "value": "4.1.4" + }, + { + "type": "paragraph", + "value": "Cool. And easy! You have to now use the second form the \"new hash\" shortcut in this exercise. Again, it takes an array of key-value pairs. These key-value pairs will just be 2-element arrays. I'll give you the key-value pairs to start with. Your objective is to build a Hash out of this array." + }, + { + "type": "test_example", + "value": "4.1.5" + } + ] + } + ] +} diff --git a/lessons/toc.json b/lessons/toc.json index 2ab07f1..3919304 100644 --- a/lessons/toc.json +++ b/lessons/toc.json @@ -11,6 +11,8 @@ "2.2.json", "3.0.json", "3.1.json", - "3.2.json" + "3.2.json", + "4.0.json", + "4.1.json" ] } diff --git a/rp b/rp index f60b39e..78f6ddf 100755 --- a/rp +++ b/rp @@ -201,12 +201,34 @@ def print_toc name = lesson_data['name'] || id entries << "#{id}: #{name}" end + max_entry_length = entries.map(&:length).max || 0 - width = 8 + max_entry_length + width = 12 + max_entry_length puts "" - puts "Table of Contents".center(width).bold.underline - entries.each do |entry| - puts " • #{entry}" + puts " " + "Table of Contents".center(width).bold.underline + + major_levels = entries.map { |e| e.split('.').first.to_i }.uniq.sort + major_levels.each do |major| + fig_lines = RubyFiglet::Figlet.new(major.to_s, 'smshadow').to_s.lines.map(&:chomp) + (0..2).each do |minor| + padded_fig_line = sprintf(" \u2502 %-6s", fig_lines[minor] || (" " * 5)) + if minor == 2 + padded_fig_line.sub!(/ $/, ". ") + end + indent = " " * (major * 2) + lesson = entries.find { |e| e.start_with?("#{major}.#{minor}:") } + if lesson + puts "#{padded_fig_line}#{indent}• #{lesson}" + else + puts "#{padded_fig_line}" + end + end + # Print extra lessons with minor > 2 + extra_lessons = entries.select { |e| e.start_with?("#{major}.") && e.split('.')[1].to_i > 2 }.sort_by { |e| e.split('.')[1].to_i } + extra_lessons.each do |lesson| + indent = " " * (major * 2) + puts "#{' ' * 6}#{indent}• #{lesson}" + end end puts "" end