diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f15e83aba --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist +.project_env.rc +.path_progress +*.rbc diff --git a/MIT-LICENSE b/MIT-LICENSE deleted file mode 100644 index b243e8cf6..000000000 --- a/MIT-LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2009 EdgeCase - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.rdoc b/README.rdoc index 5fafe07dc..fb6abb159 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,29 +1,29 @@ = EdgeCase Ruby Koans -The Ruby Koans walk you along the path to enlightenment in order to learn Ruby. -The goal is to learn the Ruby language, syntax, structure, and some common +The Ruby Koans walk you along the path to enlightenment in order to learn Ruby. +The goal is to learn the Ruby language, syntax, structure, and some common functions and libraries. We also teach you culture. Testing is not just something we -pay lip service to, but something we live. It is essential in your quest to learn -and do great things in the language. +pay lip service to, but something we live. It is essential in your quest to learn +and do great things in the language. == The Structure -The koans are broken out into areas by file, hashes are covered in about_hashes.rb, -modules are introduced in about_modules.rb, etc. They are presented in order in the -path_to_enlightenment.rb file. +The koans are broken out into areas by file, hashes are covered in about_hashes.rb, +modules are introduced in about_modules.rb, etc. They are presented in order in the +path_to_enlightenment.rb file. -Each koan builds up your knowledge of Ruby and builds upon itself. It will stop at -the first place you need to correct. +Each koan builds up your knowledge of Ruby and builds upon itself. It will stop at +the first place you need to correct. -Some koans simply need to have the correct answer substituted for an incorrect one. -Some, however, require you to supply your own answer. If you see the method +__+ (a -double underscore) listed, it is a hint to you to supply your own code in order to -make it work correctly. +Some koans simply need to have the correct answer substituted for an incorrect one. +Some, however, require you to supply your own answer. If you see the method +__+ (a +double underscore) listed, it is a hint to you to supply your own code in order to +make it work correctly. == Installing Ruby -If you do not have Ruby setup, please visit http://ruby-lang.org/en/downloads/ for -operating specific instructions. In order to run this you need ruby and rake +If you do not have Ruby setup, please visit http://ruby-lang.org/en/downloads/ for +operating specific instructions. In order to run this you need ruby and rake installed. To check the installations simply type: *nix platforms from any terminal window: @@ -33,11 +33,13 @@ installed. To check the installations simply type: Windows from the command prompt (cmd.exe) - c:\ruby --version + c:\ruby --version c:\rake --version - -Any response for Ruby with a version number greater than 1.8 is fine (should be -around 1.8.6 or more). Any version of rake will do. + +If you don't have rake installed, just run `gem install rake` + +Any response for Ruby with a version number greater than 1.8 is fine (should be +around 1.8.6 or more). Any version of rake will do. == The Path To Enlightenment @@ -48,46 +50,47 @@ recommended way to run them as we might build more functionality into this task) [ruby_koans] $ rake # runs the default target :walk_the_path [ruby_koans] $ ruby path_to_enlightenment.rb # simply call the file directly - + Windows is the same thing c:\ruby_koans\rake # runs the default target :walk_the_path c:\ruby_koans\ruby path_to_enlightenment.rb # simply call the file directly - + === Red, Green, Refactor In test-driven development the mantra has always been, red, green, refactor. Write a failing test and run it (red), make the test pass (green), then refactor it (that is look at the code and see if you can make it any better. In this case you will need -to run the koan and see it fail (refactor), make the test pass (green), then take a -moment and reflect upon the test to see what it is teaching you. +to run the koan and see it fail (red), make the test pass (green), then take a +moment and reflect upon the test to see what it is teaching you and improve the +code to better communicate its intent (refactor). The very first time you run it you will see the following output: [ ruby_koans ] $ rake (in /Users/person/dev/ruby_koans) cd koans - + Thinking AboutAsserts test_assert_truth has damaged your karma. - + You have not yet reached enlightenment ... is not true. - + Please meditate on the following code: - ./about_basics.rb:10:in `test_assert_truth' + ./about_asserts.rb:10:in `test_assert_truth' path_to_enlightenment.rb:27 mountains are merely mountains - + You have come to your first stage. If you notice it is telling you where to look for the first solution: Please meditate on the following code: - ./about_basics.rb:10:in `test_assert_truth' + ./about_asserts.rb:10:in `test_assert_truth' path_to_enlightenment.rb:27 - -We then open up the about_basics.rb file and look at the first test: + +We then open up the about_asserts.rb file and look at the first test: # We shall contemplate truth by testing reality, via asserts. def test_assert_truth @@ -96,28 +99,32 @@ We then open up the about_basics.rb file and look at the first test: We then change the +false+ to +true+ and run the test again. After you are done, think about what you are learning. In this case, ignore everything except -the method name (+test_assert_truth+) and the parts inside the method (everything -before the +end+). - -In this case the goal is for you to see that if you pass a value to the +assert+ +the method name (+test_assert_truth+) and the parts inside the method (everything +before the +end+). + +In this case the goal is for you to see that if you pass a value to the +assert+ method, it will either ensure it is +true+ and continue on, or fail if in fact -the statement is +false+. - +the statement is +false+. + == Inspiration -A special thanks to Mike Clark and Ara Howard for inspiring this project. Mike Clark -wrote an excellent blog post about learning Ruby through unit testing. This sparked -an idea that has taken a bit to solidify, that of bringing new rubyists into the -community through testing. Ara Howard then gave us the idea for the Koans in his -ruby quiz entry an Meta Koans (a must for any rubyist wanting to improve their skills). +A special thanks to Mike Clark and Ara Howard for inspiring this +project. Mike Clark wrote an excellent blog post about learning Ruby +through unit testing. This sparked an idea that has taken a bit to +solidify, that of bringing new rubyists into the community through +testing. Ara Howard then gave us the idea for the Koans in his ruby +quiz entry on Meta Koans (a must for any rubyist wanting to improve +their skills). Also, "The Little Lisper" taught us all the value of +the short questions/simple answers style of learning. Mike Clark's post :: http://www.clarkware.com/cgi/blosxom/2005/03/18 Meta Koans :: http://rubyquiz.com/quiz67.html +The Little Lisper :: http://www.amazon.com/Little-LISPer-Third-Daniel-Friedman/dp/0023397632 == Other Resources The Ruby Language :: http://ruby-lang.org -Try Ruby in your browser :: http://tryruby.hobix.com/ +Try Ruby in your browser :: http://tryruby.org Dave Thomas' introduction to Ruby Programming Ruby (the Pick Axe) :: http://pragprog.com/titles/ruby/programming-ruby @@ -125,8 +132,15 @@ Brian Marick's fantastic guide for beginners Everyday Scripting with Ruby :: = Other stuff -Author:: Jim Weirich -Author:: Joe O'Brien -Requires:: Ruby 1.8.x or later and Rake (any version) +Author :: Jim Weirich +Author :: Joe O'Brien +Issue Tracker :: http://www.pivotaltracker.com/projects/48111 +Requires :: Ruby 1.8.x or later and Rake (any recent version) + += License +http://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png +RubyKoans is released under a Creative Commons, +Attribution-NonCommercial-ShareAlike, Version 3.0 +(http://creativecommons.org/licenses/by-nc-sa/3.0/) License. diff --git a/Rakefile b/Rakefile index 0c07899f0..19b3bc18c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,84 @@ -require 'rubygems' +#!/usr/bin/env ruby +# -*- ruby -*- + +require 'rake/clean' require 'rake/rdoctask' +SRC_DIR = 'src' +PROB_DIR = 'koans' +DIST_DIR = 'dist' + +SRC_FILES = FileList["#{SRC_DIR}/*"] +KOAN_FILES = SRC_FILES.pathmap("#{PROB_DIR}/%f") + +today = Time.now.strftime("%Y-%m-%d") +TAR_FILE = "#{DIST_DIR}/rubykoans-#{today}.tgz" +ZIP_FILE = "#{DIST_DIR}/rubykoans-#{today}.zip" + +CLEAN.include("**/*.rbc") +CLOBBER.include(DIST_DIR) + +module Koans + # Remove solution info from source + # __(a,b) => __ + # _n_(number) => __ + # # __ => + def Koans.remove_solution(line) + line = line.gsub(/\b____\([^\)]+\)/, "____") + line = line.gsub(/\b___\([^\)]+\)/, "___") + line = line.gsub(/\b__\([^\)]+\)/, "__") + line = line.gsub(/\b_n_\([^\)]+\)/, "_n_") + line = line.gsub(%r(/\#\{__\}/), "/__/") + line = line.gsub(/\s*#\s*__\s*$/, '') + line + end + + def Koans.make_koan_file(infile, outfile) + if infile =~ /edgecase/ + cp infile, outfile + elsif infile =~ /autotest/ + cp_r infile, outfile + else + open(infile) do |ins| + open(outfile, "w") do |outs| + state = :copy + ins.each do |line| + state = :skip if line =~ /^ *#--/ + case state + when :copy + outs.puts remove_solution(line) + else + # do nothing + end + state = :copy if line =~ /^ *#\+\+/ + end + end + end + end + end +end + +module RubyImpls + # Calculate the list of relevant Ruby implementations. + def self.find_ruby_impls + rubys = `rvm list`.gsub(/=>/,'').split(/\n/).sort + expected.map { |impl| + last = rubys.grep(Regexp.new(Regexp.quote(impl))).last + last ? last.split.first : nil + }.compact + end + + # Return a (cached) list of relevant Ruby implementations. + def self.list + @list ||= find_ruby_impls + end + + # List of expected ruby implementations. + def self.expected + %w(ruby-1.8.6 ruby-1.8.7 ruby-1.9.2 jruby ree) + end +end + task :default => :walk_the_path task :walk_the_path do @@ -12,3 +90,77 @@ Rake::RDocTask.new do |rd| rd.main = "README.rdoc" rd.rdoc_files.include("README.rdoc", "koans/*.rb") end + +directory DIST_DIR +directory PROB_DIR + +file ZIP_FILE => KOAN_FILES + [DIST_DIR] do + sh "zip #{ZIP_FILE} #{PROB_DIR}/*" +end + +file TAR_FILE => KOAN_FILES + [DIST_DIR] do + sh "tar zcvf #{TAR_FILE} #{PROB_DIR}" +end + +desc "Create packaged files for distribution" +task :package => [TAR_FILE, ZIP_FILE] + +desc "Upload the package files to the web server" +task :upload => [TAR_FILE, ZIP_FILE] do + sh "scp #{TAR_FILE} linode:sites/onestepback.org/download" + sh "scp #{ZIP_FILE} linode:sites/onestepback.org/download" +end + +desc "Generate the Koans from the source files from scratch." +task :regen => [:clobber_koans, :gen] + +desc "Generate the Koans from the changed source files." +task :gen => KOAN_FILES + [PROB_DIR + "/README.rdoc"] +task :clobber_koans do + rm_r PROB_DIR +end + +file PROB_DIR + "/README.rdoc" => "README.rdoc" do |t| + cp "README.rdoc", t.name +end + +SRC_FILES.each do |koan_src| + file koan_src.pathmap("#{PROB_DIR}/%f") => [PROB_DIR, koan_src] do |t| + Koans.make_koan_file koan_src, t.name + end +end + +task :run do + puts 'koans' + Dir.chdir("src") do + puts "in #{Dir.pwd}" + sh "ruby path_to_enlightenment.rb" + end +end + + +desc "Pre-checkin tests (=> run_all)" +task :cruise => :run_all + +desc "Run the completed koans againts a list of relevant Ruby Implementations" +task :run_all do + results = [] + RubyImpls.list.each do |impl| + puts "=" * 40 + puts "On Ruby #{impl}" + sh "rvm #{impl} rake run" + results << [impl, "RAN"] + puts + end + puts "=" * 40 + puts "Summary:" + puts + results.each do |impl, res| + puts "#{impl} => RAN" + end + puts + RubyImpls.expected.each do |requested_impl| + impl_pattern = Regexp.new(Regexp.quote(requested_impl)) + puts "No Results for #{requested_impl}" unless results.detect { |x| x.first =~ impl_pattern } + end +end diff --git a/keynote/RubyKoans.key b/keynote/RubyKoans.key new file mode 100644 index 000000000..a9f00ac95 Binary files /dev/null and b/keynote/RubyKoans.key differ diff --git a/koans/GREED_RULES.txt b/koans/GREED_RULES.txt index f120604d5..58b5a9cb6 100644 --- a/koans/GREED_RULES.txt +++ b/koans/GREED_RULES.txt @@ -1,12 +1,12 @@ = Playing Greed -Greed is a dice game played amoung 2 or more players, using 5 +Greed is a dice game played among 2 or more players, using 5 six-sided dice. == Playing Greed Each player takes a turn consisting of one or more rolls of the dice. -On the first roll of the game, a player rolls all six dice which are +On the first roll of the game, a player rolls all five dice which are scored according to the following: Three 1's => 1000 points @@ -37,8 +37,8 @@ final example. After a player rolls and the score is calculated, the scoring dice are removed and the player has the option of rolling again using only the -non-scoring dice. If there all no non-scoring dice), then the player -may roll all 5 dice in the next roll. +non-scoring dice. If all of the thrown dice are scoring, then the +player may roll all 5 dice in the next roll. The player may continue to roll as long as each roll scores points. If a roll has zero points, then the player loses not only their turn, but diff --git a/koans/README.rdoc b/koans/README.rdoc new file mode 100644 index 000000000..f1e0f8e4a --- /dev/null +++ b/koans/README.rdoc @@ -0,0 +1,136 @@ += EdgeCase Ruby Koans + +The Ruby Koans walk you along the path to enlightenment in order to learn Ruby. +The goal is to learn the Ruby language, syntax, structure, and some common +functions and libraries. We also teach you culture. Testing is not just something we +pay lip service to, but something we live. It is essential in your quest to learn +and do great things in the language. + +== The Structure + +The koans are broken out into areas by file, hashes are covered in about_hashes.rb, +modules are introduced in about_modules.rb, etc. They are presented in order in the +path_to_enlightenment.rb file. + +Each koan builds up your knowledge of Ruby and builds upon itself. It will stop at +the first place you need to correct. + +Some koans simply need to have the correct answer substituted for an incorrect one. +Some, however, require you to supply your own answer. If you see the method +__+ (a +double underscore) listed, it is a hint to you to supply your own code in order to +make it work correctly. + +== Installing Ruby + +If you do not have Ruby setup, please visit http://ruby-lang.org/en/downloads/ for +operating specific instructions. In order to run this you need ruby and rake +installed. To check the installations simply type: + +*nix platforms from any terminal window: + + [~] $ ruby --version + [~] $ rake --version + +Windows from the command prompt (cmd.exe) + + c:\ruby --version + c:\rake --version + +Any response for Ruby with a version number greater than 1.8 is fine (should be +around 1.8.6 or more). Any version of rake will do. + +== The Path To Enlightenment + +You can run the tests through rake or by calling the file itself (rake is the +recommended way to run them as we might build more functionality into this task). + +*nix platforms, from the koans directory + + [ruby_koans] $ rake # runs the default target :walk_the_path + [ruby_koans] $ ruby path_to_enlightenment.rb # simply call the file directly + +Windows is the same thing + + c:\ruby_koans\rake # runs the default target :walk_the_path + c:\ruby_koans\ruby path_to_enlightenment.rb # simply call the file directly + +=== Red, Green, Refactor + +In test-driven development the mantra has always been, red, green, refactor. Write a +failing test and run it (red), make the test pass (green), then refactor it (that is +look at the code and see if you can make it any better. In this case you will need +to run the koan and see it fail (red), make the test pass (green), then take a +moment and reflect upon the test to see what it is teaching you and improve the +code to better communicate its intent (refactor). + +The very first time you run it you will see the following output: + + [ ruby_koans ] $ rake + (in /Users/person/dev/ruby_koans) + cd koans + + Thinking AboutAsserts + test_assert_truth has damaged your karma. + + You have not yet reached enlightenment ... + is not true. + + Please meditate on the following code: + ./about_asserts.rb:10:in `test_assert_truth' + path_to_enlightenment.rb:27 + + mountains are merely mountains + +You have come to your first stage. If you notice it is telling you where to look for +the first solution: + + Please meditate on the following code: + ./about_asserts.rb:10:in `test_assert_truth' + path_to_enlightenment.rb:27 + +We then open up the about_asserts.rb file and look at the first test: + + # We shall contemplate truth by testing reality, via asserts. + def test_assert_truth + assert false # This should be true + end + +We then change the +false+ to +true+ and run the test again. After you are +done, think about what you are learning. In this case, ignore everything except +the method name (+test_assert_truth+) and the parts inside the method (everything +before the +end+). + +In this case the goal is for you to see that if you pass a value to the +assert+ +method, it will either ensure it is +true+ and continue on, or fail if in fact +the statement is +false+. + +== Inspiration + +A special thanks to Mike Clark and Ara Howard for inspiring this +project. Mike Clark wrote an excellent blog post about learning Ruby +through unit testing. This sparked an idea that has taken a bit to +solidify, that of bringing new rubyists into the community through +testing. Ara Howard then gave us the idea for the Koans in his ruby +quiz entry on Meta Koans (a must for any rubyist wanting to improve +their skills). Also, "The Little Lisper" taught us all the value of +the short questions/simple answers style of learning. + +Mike Clark's post :: http://www.clarkware.com/cgi/blosxom/2005/03/18 +Meta Koans :: http://rubyquiz.com/quiz67.html +The Little Lisper :: http://www.amazon.com/Little-LISPer-Third-Daniel-Friedman/dp/0023397632 + +== Other Resources + +The Ruby Language :: http://ruby-lang.org +Try Ruby in your browser :: http://tryruby.org + +Dave Thomas' introduction to Ruby Programming Ruby (the Pick Axe) :: http://pragprog.com/titles/ruby/programming-ruby + +Brian Marick's fantastic guide for beginners Everyday Scripting with Ruby :: http://pragprog.com/titles/bmsft/everyday-scripting-with-ruby + += Other stuff + +Author :: Jim Weirich +Author :: Joe O'Brien +Issue Tracker :: http://www.pivotaltracker.com/projects/48111 +Requires :: Ruby 1.8.x or later and Rake (any recent version) diff --git a/koans/Rakefile b/koans/Rakefile new file mode 100644 index 000000000..1a2c7f26d --- /dev/null +++ b/koans/Rakefile @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +# -*- ruby -*- + +require 'rake/clean' +require 'rake/testtask' + +task :default => :test + +task :test do + ruby 'path_to_enlightenment.rb' +end + diff --git a/koans/about_array_assignment.rb b/koans/about_array_assignment.rb index 37b1068c5..21a64bb81 100644 --- a/koans/about_array_assignment.rb +++ b/koans/about_array_assignment.rb @@ -1,11 +1,11 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutArrayAssignment < EdgeCase::Koan def test_non_parallel_assignment names = ["John", "Smith"] assert_equal __, names end - + def test_parallel_assignments first_name, last_name = ["John", "Smith"] assert_equal __, first_name @@ -18,13 +18,19 @@ def test_parallel_assignments_with_extra_values assert_equal __, last_name end - def test_parallel_assignments_with_extra_variables + def test_parallel_assignments_with_splat_operator + first_name, *last_name = ["John", "Smith", "III"] + assert_equal __, first_name + assert_equal __, last_name + end + + def test_parallel_assignments_with_too_few_variables first_name, last_name = ["Cher"] assert_equal __, first_name assert_equal __, last_name end - def test_parallel_assignements_with_subarrays + def test_parallel_assignments_with_subarrays first_name, last_name = [["Willie", "Rae"], "Johnson"] assert_equal __, first_name assert_equal __, last_name @@ -35,4 +41,11 @@ def test_parallel_assignment_with_one_variable assert_equal __, first_name end + def test_swapping_with_parallel_assignment + first_name = "Roy" + last_name = "Rob" + first_name, last_name = last_name, first_name + assert_equal __, first_name + assert_equal __, last_name + end end diff --git a/koans/about_arrays.rb b/koans/about_arrays.rb index 5fd016af5..9f27b5aaa 100644 --- a/koans/about_arrays.rb +++ b/koans/about_arrays.rb @@ -1,9 +1,9 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutArrays < EdgeCase::Koan def test_creating_arrays empty_array = Array.new - assert_equal Array, empty_array.class + assert_equal __, empty_array.class assert_equal __, empty_array.size end @@ -40,13 +40,14 @@ def test_slicing_arrays assert_equal __, array[2,2] assert_equal __, array[2,20] assert_equal __, array[4,0] + assert_equal __, array[4,100] assert_equal __, array[5,0] end def test_arrays_and_ranges - assert_equal Range, (1..5).class + assert_equal __, (1..5).class assert_not_equal [1,2,3,4,5], (1..5) - assert_equal [1,2,3,4,5], (1..5).to_a + assert_equal __, (1..5).to_a assert_equal __, (1...5).to_a end @@ -80,22 +81,4 @@ def test_shifting_arrays assert_equal __, array end - def test_parallel_assignments - first_name, last_name = ["John", "Smith"] - assert_equal __, first_name - assert_equal __, last_name - end - - def test_parallel_assignments_with_extra_values - first_name, last_name = ["John", "Smith", "III"] - assert_equal __, first_name - assert_equal __, last_name - end - - def test_parallel_assignments_with_extra_variables - first_name, last_name = ["Cher"] - assert_equal __, first_name - assert_equal __, last_name - end - end diff --git a/koans/about_basics.rb b/koans/about_asserts.rb similarity index 88% rename from koans/about_basics.rb rename to koans/about_asserts.rb index db0bfe373..e1c34d44e 100644 --- a/koans/about_basics.rb +++ b/koans/about_asserts.rb @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # -*- ruby -*- -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutAsserts < EdgeCase::Koan @@ -19,7 +19,7 @@ def test_assert_with_message # To understand reality, we must compare our expectations against # reality. def test_assert_equality - expected_value = 3 + expected_value = __ actual_value = 1 + 1 assert expected_value == actual_value @@ -27,7 +27,7 @@ def test_assert_equality # Some ways of asserting equality are better than others. def test_a_better_way_of_asserting_equality - expected_value = 3 + expected_value = __ actual_value = 1 + 1 assert_equal expected_value, actual_value diff --git a/koans/about_blocks.rb b/koans/about_blocks.rb index df3a745d2..0abee8f9b 100644 --- a/koans/about_blocks.rb +++ b/koans/about_blocks.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutBlocks < EdgeCase::Koan def method_with_block @@ -60,7 +60,7 @@ def test_methods_can_see_if_they_have_been_called_with_a_block # ------------------------------------------------------------------ - def test_block_can_effect_variables_in_the_code_where_they_are_created + def test_block_can_affect_variables_in_the_code_where_they_are_created value = :initial_value method_with_block { value = :modified_in_a_block } assert_equal __, value @@ -77,20 +77,20 @@ def test_blocks_can_be_assigned_to_variables_and_called_explicitly def test_stand_alone_blocks_can_be_passed_to_methods_expecting_blocks make_upper = lambda { |n| n.upcase } result = method_with_block_arguments(&make_upper) - assert_equal __, result + assert_equal __, result end # ------------------------------------------------------------------ - def method_with_explict_block(&block) + def method_with_explicit_block(&block) block.call(10) end def test_methods_can_take_an_explicit_block_argument - assert_equal __, method_with_explict_block { |n| n * 2 } + assert_equal __, method_with_explicit_block { |n| n * 2 } add_one = lambda { |n| n + 1 } - assert_equal __, method_with_explict_block(&add_one) + assert_equal __, method_with_explicit_block(&add_one) end end diff --git a/koans/about_class_methods.rb b/koans/about_class_methods.rb index c3184032d..ddd32bd51 100644 --- a/koans/about_class_methods.rb +++ b/koans/about_class_methods.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutClassMethods < EdgeCase::Koan class Dog @@ -9,7 +9,7 @@ def test_objects_are_objects assert_equal __, fido.is_a?(Object) end - def test_classes_are_objects_too + def test_classes_are_classes assert_equal __, Dog.is_a?(Class) end @@ -19,11 +19,11 @@ def test_classes_are_objects_too def test_objects_have_methods fido = Dog.new - assert_equal __, fido.methods.size + assert fido.methods.size > _n_ end def test_classes_have_methods - assert_equal __, Dog.methods.size + assert Dog.methods.size > _n_ end def test_you_can_define_methods_on_individual_objects @@ -34,7 +34,7 @@ def fido.wag assert_equal __, fido.wag end - def test_other_objects_are_affected_by_these_singleton_methods + def test_other_objects_are_not_affected_by_these_singleton_methods fido = Dog.new rover = Dog.new def fido.wag @@ -48,24 +48,24 @@ def fido.wag # ------------------------------------------------------------------ - def Dog.wag - :class_level_wag - end - - class Dog + class Dog2 def wag :instance_level_wag end end + def Dog2.wag + :class_level_wag + end + def test_since_classes_are_objects_you_can_define_singleton_methods_on_them_too - assert_equal __, Dog.a_class_method + assert_equal __, Dog2.wag end def test_class_methods_are_independent_of_instance_methods - fido = Dog.new + fido = Dog2.new assert_equal __, fido.wag - assert_equal __, Dog.wag + assert_equal __, Dog2.wag end # ------------------------------------------------------------------ diff --git a/koans/about_classes.rb b/koans/about_classes.rb index d576e4bbb..fce0be084 100644 --- a/koans/about_classes.rb +++ b/koans/about_classes.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutClasses < EdgeCase::Koan class Dog @@ -43,7 +43,7 @@ def test_you_can_politely_ask_for_instance_variable_values fido = Dog2.new fido.set_name("Fido") - assert_equal __, fido.instance_variable_get("@name") + assert_equal __, fido.instance_variable_get("@name") end def test_you_can_rip_the_value_out_using_instance_eval @@ -89,7 +89,7 @@ def test_attr_reader_will_automatically_define_an_accessor assert_equal __, fido.name end - + # ------------------------------------------------------------------ class Dog5 @@ -125,12 +125,12 @@ def test_args_to_new_must_match_initialize # THINK ABOUT IT: # Why is this so? end - + def test_different_objects_have_difference_instance_variables fido = Dog6.new("Fido") rover = Dog6.new("Rover") - assert_not_equal rover.name, fido.name + assert_equal __, rover.name != fido.name end # ------------------------------------------------------------------ @@ -164,12 +164,12 @@ def test_inside_a_method_self_refers_to_the_containing_object def test_to_s_provides_a_string_version_of_the_object fido = Dog7.new("Fido") - assert_equal "Fido", fido.to_s + assert_equal __, fido.to_s end def test_to_s_is_used_in_string_interpolation fido = Dog7.new("Fido") - assert_equal "My dog is Fido", "My dog is #{fido}" + assert_equal __, "My dog is #{fido}" end def test_inspect_provides_a_more_complete_string_version @@ -186,5 +186,5 @@ def test_all_objects_support_to_s_and_inspect assert_equal __, "STRING".to_s assert_equal __, "STRING".inspect end - + end diff --git a/koans/about_constants.rb b/koans/about_constants.rb new file mode 100644 index 000000000..41d3f0126 --- /dev/null +++ b/koans/about_constants.rb @@ -0,0 +1,87 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +C = "top level" + +class AboutConstants < EdgeCase::Koan + + C = "nested" + + def test_nested_constants_may_also_be_referenced_with_relative_paths + assert_equal __, C + end + + def test_top_level_constants_are_referenced_by_double_colons + assert_equal __, ::C + end + + def test_nested_constants_are_referenced_by_their_complete_path + assert_equal __, AboutConstants::C + assert_equal __, ::AboutConstants::C + end + + # ------------------------------------------------------------------ + + class Animal + LEGS = 4 + def legs_in_animal + LEGS + end + + class NestedAnimal + def legs_in_nested_animal + LEGS + end + end + end + + def test_nested_classes_inherit_constants_from_enclosing_classes + assert_equal __, Animal::NestedAnimal.new.legs_in_nested_animal + end + + # ------------------------------------------------------------------ + + class Reptile < Animal + def legs_in_reptile + LEGS + end + end + + def test_subclasses_inherit_constants_from_parent_classes + assert_equal __, Reptile.new.legs_in_reptile + end + + # ------------------------------------------------------------------ + + class MyAnimals + LEGS = 2 + + class Bird < Animal + def legs_in_bird + LEGS + end + end + end + + def test_who_wins_with_both_nested_and_inherited_constants + assert_equal __, MyAnimals::Bird.new.legs_in_bird + end + + # QUESTION: Which has precedence: The constant in the lexical scope, + # or the constant from the inheritance hierarchy? + + # ------------------------------------------------------------------ + + class MyAnimals::Oyster < Animal + def legs_in_oyster + LEGS + end + end + + def test_who_wins_with_explicit_scoping_on_class_definition + assert_equal __, MyAnimals::Oyster.new.legs_in_oyster + end + + # QUESTION: Now which has precedence: The constant in the lexical + # scope, or the constant from the inheritance hierarchy? Why is it + # different than the previous answer? +end diff --git a/koans/about_control_statements.rb b/koans/about_control_statements.rb index d2b75ad08..f243ac85b 100644 --- a/koans/about_control_statements.rb +++ b/koans/about_control_statements.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutControlStatements < EdgeCase::Koan @@ -11,7 +11,7 @@ def test_if_then_else_statements assert_equal __, result end - def test_if_then_else_statements + def test_if_then_statements result = :default_value if true result = :true_value @@ -93,6 +93,16 @@ def test_break_statement assert_equal __, result end + def test_break_statement_returns_values + i = 1 + result = while i <= 10 + break i if i % 2 == 0 + i += 1 + end + + assert_equal __, result + end + def test_next_statement i = 0 result = [] diff --git a/koans/about_dice_project.rb b/koans/about_dice_project.rb index c1fccb14c..ce817155e 100644 --- a/koans/about_dice_project.rb +++ b/koans/about_dice_project.rb @@ -1,13 +1,12 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') -class DiceSet - attr_reader :values - def roll(n) - @values = (1..n).map { rand(6) + 1 } - end -end +# Implement a DiceSet Class here: +# +# class DiceSet +# code ... +# end -class AboutDiceSet < EdgeCase::Koan +class AboutDiceProject < EdgeCase::Koan def test_can_create_a_dice_set dice = DiceSet.new assert_not_nil dice diff --git a/koans/about_exceptions.rb b/koans/about_exceptions.rb index b2843b118..d745f961b 100644 --- a/koans/about_exceptions.rb +++ b/koans/about_exceptions.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutExceptions < EdgeCase::Koan @@ -6,10 +6,10 @@ class MySpecialError < RuntimeError end def test_exceptions_inherit_from_Exception - assert MySpecialError.ancestors.include?(RuntimeError) - assert MySpecialError.ancestors.include?(StandardError) - assert MySpecialError.ancestors.include?(Exception) - assert MySpecialError.ancestors.include?(Object) + assert_equal __, MySpecialError.ancestors[1] + assert_equal __, MySpecialError.ancestors[2] + assert_equal __, MySpecialError.ancestors[3] + assert_equal __, MySpecialError.ancestors[4] end def test_rescue_clause @@ -22,12 +22,12 @@ def test_rescue_clause assert_equal __, result - assert ex.is_a?(StandardError), "Failure message." - assert ex.is_a?(RuntimeError), "Failure message." + assert_equal __, ex.is_a?(StandardError), "Should be a Standard Error" + assert_equal __, ex.is_a?(RuntimeError), "Should be a Runtime Error" assert RuntimeError.ancestors.include?(StandardError), "RuntimeError is a subclass of StandardError" - + assert_equal __, ex.message end @@ -40,7 +40,7 @@ def test_raising_a_particular_error result = :exception_handled end - assert_equal __(:exception_handled), result + assert_equal __, result assert_equal __, ex.message end @@ -57,4 +57,12 @@ def test_ensure_clause assert_equal __, result end + # Sometimes, we must know about the unknown + def test_asserting_an_error_is_raised + # A do-end is a block, a topic to explore more later + assert_raise(___) do + raise MySpecialError.new("New instances can be raised directly.") + end + end + end diff --git a/koans/about_hashes.rb b/koans/about_hashes.rb index 6208f3879..2324b0460 100644 --- a/koans/about_hashes.rb +++ b/koans/about_hashes.rb @@ -1,9 +1,9 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutHashes < EdgeCase::Koan def test_creating_hashes empty_hash = Hash.new - assert_equal Hash, empty_hash.class + assert_equal __, empty_hash.class assert_equal({}, empty_hash) assert_equal __, empty_hash.size end @@ -25,7 +25,7 @@ def test_changing_hashes hash[:one] = "eins" expected = { :one => __, :two => "dos" } - assert_equal expected, hash + assert_equal __, expected == hash # Bonus Question: Why was "expected" broken out into a variable # rather than used as a literal? @@ -35,22 +35,46 @@ def test_hash_is_unordered hash1 = { :one => "uno", :two => "dos" } hash2 = { :two => "dos", :one => "uno" } - assert_equal hash1, hash2 + assert_equal __, hash1 == hash2 end - def test_hash_keys_and_values + def test_hash_keys hash = { :one => "uno", :two => "dos" } - assert_equal __, hash.keys - assert_equal __, hash.values + assert_equal __, hash.keys.size + assert_equal __, hash.keys.include?(:one) + assert_equal __, hash.keys.include?(:two) + assert_equal __, hash.keys.class + end + + def test_hash_values + hash = { :one => "uno", :two => "dos" } + assert_equal __, hash.values.size + assert_equal __, hash.values.include?("uno") + assert_equal __, hash.values.include?("dos") + assert_equal __, hash.values.class end def test_combining_hashes hash = { "jim" => 53, "amy" => 20, "dan" => 23 } new_hash = hash.merge({ "jim" => 54, "jenny" => 26 }) - assert_not_equal hash, new_hash - + assert_equal __, hash != new_hash + expected = { "jim" => __, "amy" => 20, "dan" => 23, "jenny" => __ } - assert_equal expected, new_hash + assert_equal __, expected == new_hash + end + + def test_default_value + hash1 = Hash.new + hash1[:one] = 1 + + assert_equal __, hash1[:one] + assert_equal __, hash1[:two] + + hash2 = Hash.new("dos") + hash2[:one] = 1 + + assert_equal __, hash2[:one] + assert_equal __, hash2[:two] end end diff --git a/koans/about_inheritance.rb b/koans/about_inheritance.rb index 7bc3a1f99..712daca55 100644 --- a/koans/about_inheritance.rb +++ b/koans/about_inheritance.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutInheritance < EdgeCase::Koan class Dog @@ -31,7 +31,7 @@ def test_all_classes_ultimately_inherit_from_object assert_equal __, Chihuahua.ancestors.include?(Object) end - def test_subcases_inherit_behavior_from_parent_class + def test_subclasses_inherit_behavior_from_parent_class chico = Chihuahua.new("Chico") assert_equal __, chico.name end @@ -60,10 +60,6 @@ class BullDog < Dog def bark super + ", GROWL" end - - def growl - super.bark + ", GROWL" - end end def test_subclasses_can_invoke_parent_behavior_via_super @@ -71,11 +67,6 @@ def test_subclasses_can_invoke_parent_behavior_via_super assert_equal __, ralph.bark end - def test_super_does_not_work_cross_method - ralph = BullDog.new("Ralph") - - end - # ------------------------------------------------------------------ class GreatDane < Dog diff --git a/koans/about_iteration.rb b/koans/about_iteration.rb index 01808dcdb..b48c27813 100644 --- a/koans/about_iteration.rb +++ b/koans/about_iteration.rb @@ -1,9 +1,9 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutIteration < EdgeCase::Koan def test_each_is_a_method_on_arrays - [].methods.include?("each") + assert_equal __, [].methods.include?(:each) end def test_iterating_with_each @@ -12,7 +12,7 @@ def test_iterating_with_each array.each do |item| sum += item end - assert_equal 6, sum + assert_equal __, sum end def test_each_can_use_curly_brace_blocks_too @@ -65,7 +65,7 @@ def test_inject_will_blow_your_mind result = [2, 3, 4].inject(0) { |sum, item| sum + item } assert_equal __, result - result2 = [2, 3, 4].inject(1) { |sum, item| sum * item } + result2 = [2, 3, 4].inject(1) { |product, item| product * item } assert_equal __, result2 # Extra Credit: @@ -78,16 +78,26 @@ def test_all_iteration_methods_work_on_any_collection_not_just_arrays assert_equal __, result # Files act like a collection of lines - file = File.open("example_file.txt") - upcase_lines = file.map { |line| line.strip.upcase } - assert_equal __, upcase_lines + File.open("example_file.txt") do |file| + upcase_lines = file.map { |line| line.strip.upcase } + assert_equal __, upcase_lines + end # NOTE: You can create your own collections that work with each, # map, select, etc. - ensure - # Arg, this is ugly. - # We will figure out how to fix this later. - file.close if file end + # Bonus Question: In the previous koan, we saw the construct: + # + # File.open(filename) do |file| + # # code to read 'file' + # end + # + # Why did we do it that way instead of the following? + # + # file = File.open(filename) + # # code to read 'file' + # + # When you get to the "AboutSandwichCode" koan, recheck your answer. + end diff --git a/koans/about_java_interop.rb b/koans/about_java_interop.rb new file mode 100644 index 000000000..4d35d5dd4 --- /dev/null +++ b/koans/about_java_interop.rb @@ -0,0 +1,137 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +include Java + +# Concepts +# * Pull in a java class +# * calling a method, Camel vs snake +# * Resolving module/class name conflicts +# * Showing what gets returned +# * Ruby Strings VS Java Strings +# * Calling custom java class +# * Calling Ruby from java??? + +class AboutJavaInterop < EdgeCase::Koan + def test_using_a_java_library_class + java_array = java.util.ArrayList.new + assert_equal __, java_array.class + end + + def test_java_class_can_be_referenced_using_both_ruby_and_java_like_syntax + assert_equal __, Java::JavaUtil::ArrayList == java.util.ArrayList + end + + def test_include_class_includes_class_in_module_scope + assert_nil defined?(TreeSet) + include_class "java.util.TreeSet" + assert_equal __, defined?(TreeSet) + end + + # THINK ABOUT IT: + # + # What if we use: + # + # include_class "java.lang.String" + # + # What would be the value of the String constant after this + # include_class is run? Would it be useful to provide a way of + # aliasing java classes to different names? + + JString = java.lang.String + def test_also_java_class_can_be_given_ruby_aliases + java_string = JString.new("A Java String") + assert_equal __, java_string.class + assert_equal __, JString + end + + def test_can_directly_call_java_methods_on_java_objects + java_string = JString.new("A Java String") + assert_equal __, java_string.toLowerCase + end + + def test_jruby_provides_snake_case_versions_of_java_methods + java_string = JString.new("A Java String") + assert_equal __, java_string.to_lower_case + end + + def test_jruby_provides_question_mark_versions_of_boolean_methods + java_string = JString.new("A Java String") + assert_equal __, java_string.endsWith("String") + assert_equal __, java_string.ends_with("String") + assert_equal __, java_string.ends_with?("String") + end + + def test_java_string_are_not_ruby_strings + ruby_string = "A Java String" + java_string = java.lang.String.new(ruby_string) + assert_equal __, java_string.is_a?(java.lang.String) + assert_equal __, java_string.is_a?(String) + end + + def test_java_strings_can_be_compared_to_ruby_strings_maybe + ruby_string = "A Java String" + java_string = java.lang.String.new(ruby_string) + assert_equal __, ruby_string == java_string + assert_equal __, java_string == ruby_string + + # THINK ABOUT IT: + # + # Is there any possible way for this to be more wrong? + # + # SERIOUSLY, THINK ABOUT IT: + # + # Why do you suppose that Ruby and Java strings compare like that? + # + # ADVANCED THINK ABOUT IT: + # + # Is there a way to make Ruby/Java string comparisons commutative? + # How would you do it? + end + + def test_however_most_methods_returning_strings_return_ruby_strings + java_array = java.util.ArrayList.new + assert_equal __, java_array.toString + assert_equal __, java_array.toString.is_a?(String) + assert_equal __, java_array.toString.is_a?(java.lang.String) + end + + def test_some_ruby_objects_can_be_coerced_to_java + assert_equal __, "ruby string".to_java.class + assert_equal __, 1.to_java.class + assert_equal __, 9.32.to_java.class + assert_equal __, false.to_java.class + end + + def test_some_ruby_objects_are_not_coerced_to_what_you_might_expect + assert_equal __, [].to_java.class == Java::JavaUtil::ArrayList + assert_equal __, {}.to_java.class == Java::JavaUtil::HashMap + assert_equal __, Object.new.to_java.class == Java::JavaLang::Object + end + + def test_java_collections_are_enumerable + java_array = java.util.ArrayList.new + java_array << "one" << "two" << "three" + assert_equal __, java_array.map { |item| item.upcase } + end + + # ------------------------------------------------------------------ + + # Open the Java ArrayList class and add a new method. + class Java::JavaUtil::ArrayList + def multiply_all + result = 1 + each do |item| + result *= item + end + result + end + end + + def test_java_class_are_open_from_ruby + java_array = java.util.ArrayList.new + java_array.add_all([1,2,3,4,5]) + + assert_equal __, java_array.multiply_all + end + +end diff --git a/koans/about_message_passing.rb b/koans/about_message_passing.rb index d396c8034..a978ddec4 100644 --- a/koans/about_message_passing.rb +++ b/koans/about_message_passing.rb @@ -1,31 +1,31 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutMessagePassing < EdgeCase::Koan - + class MessageCatcher def caught? true end end - + def test_methods_can_be_called_directly mc = MessageCatcher.new - - assert mc.caught? + + assert mc.caught? end - + def test_methods_can_be_invoked_by_sending_the_message mc = MessageCatcher.new - + assert mc.send(:caught?) end - + def test_methods_can_be_invoked_more_dynamically mc = MessageCatcher.new - + assert mc.send("caught?") assert mc.send("caught" + __ ) # What do you need to add to the first string? - assert mc.send("CAUGHT?".__ ) # What would you need to do to the string? + assert mc.send("CAUGHT?".____ ) # What would you need to do to the string? end def test_send_with_underscores_will_also_send_messages @@ -40,23 +40,22 @@ def test_send_with_underscores_will_also_send_messages def test_classes_can_be_asked_if_they_know_how_to_respond mc = MessageCatcher.new - + assert_equal __, mc.respond_to?(:caught?) assert_equal __, mc.respond_to?(:does_not_exist) end - + # ------------------------------------------------------------------ class MessageCatcher def add_a_payload(*args) - return :empty unless args args end end - + def test_sending_a_message_with_arguments mc = MessageCatcher.new - + assert_equal __, mc.add_a_payload assert_equal __, mc.send(:add_a_payload) @@ -72,7 +71,7 @@ class TypicalObject def test_sending_undefined_messages_to_a_typical_object_results_in_errors typical = TypicalObject.new - assert_raise(___) do + exception = assert_raise(___) do typical.foobar end assert_match(/foobar/, exception.message) @@ -90,13 +89,25 @@ def test_calling_method_missing_causes_the_no_method_error # # If the method :method_missing causes the NoMethodError, then # what would happen if we redefine method_missing? + # + # NOTE: + # + # In Ruby 1.8 the method_missing method is public and can be + # called as shown above. However, in Ruby 1.9 the method_missing + # method is private. We explicitly made it public in the testing + # framework so this example works in both versions of Ruby. Just + # keep in mind you can't call method_missing like that in Ruby + # 1.9. normally. + # + # Thanks. We now return you to your regularly scheduled Ruby + # Koans. end # ------------------------------------------------------------------ class AllMessageCatcher def method_missing(method_name, *args, &block) - "Someone called #{method_name} with (#{args.join(", ")})" + "Someone called #{method_name} with <#{args.join(", ")}>" end end diff --git a/koans/about_methods.rb b/koans/about_methods.rb index 187ad27dd..a4df4b8e5 100644 --- a/koans/about_methods.rb +++ b/koans/about_methods.rb @@ -1,24 +1,24 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') def my_global_method(a,b) a + b end - + class AboutMethods < EdgeCase::Koan def test_calling_global_methods assert_equal __, my_global_method(2,3) end - def test_calling_global_methods_without_parenthesis + def test_calling_global_methods_without_parentheses result = my_global_method 2, 3 assert_equal __, result end # (NOTE: We are Using eval below because the example code is # considered to be syntactically invalid). - def test_sometimes_missing_parenthesis_are_ambiguous - eval "assert_equal 5, my_global_method 2, 3" + def test_sometimes_missing_parentheses_are_ambiguous + eval "assert_equal 5, my_global_method 2, 3" # ENABLE CHECK # # Ruby doesn't know if you mean: # @@ -29,19 +29,19 @@ def test_sometimes_missing_parenthesis_are_ambiguous # Rewrite the eval string to continue. # end - + # NOTE: wrong number of argument is not a SYNTAX error, but a # runtime error. def test_calling_global_methods_with_wrong_number_of_arguments exception = assert_raise(___) do my_global_method end - assert_equal __, exception.message + assert_match(/__/, exception.message) exception = assert_raise(___) do my_global_method(1,2,3) end - assert_equal __, exception.message + assert_match(/__/, exception.message) end # ------------------------------------------------------------------ @@ -72,7 +72,7 @@ def test_calling_with_variable_arguments def method_with_explicit_return :a_non_return_value return :return_value - :anoher_non_return_value + :another_non_return_value end def test_method_with_explicit_return @@ -92,16 +92,16 @@ def test_method_without_explicit_return # ------------------------------------------------------------------ - def my_same_class_method(a, b) + def my_method_in_the_same_class(a, b) a * b end def test_calling_methods_in_same_class - assert_equal __, my_same_class_method(3,4) + assert_equal __, my_method_in_the_same_class(3,4) end def test_calling_methods_in_same_class_with_explicit_receiver - assert_equal __, self.my_same_class_method(3,4) + assert_equal __, self.my_method_in_the_same_class(3,4) end # ------------------------------------------------------------------ @@ -135,7 +135,7 @@ def tail "tail" end end - + def test_calling_methods_in_other_objects_require_explicit_receiver rover = Dog.new assert_equal __, rover.name diff --git a/koans/about_modules.rb b/koans/about_modules.rb index c18c81e30..8b56b65c0 100644 --- a/koans/about_modules.rb +++ b/koans/about_modules.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutModules < EdgeCase::Koan module Nameable @@ -42,10 +42,10 @@ def test_normal_methods_are_available_in_the_object assert_equal __, fido.bark end - def test_module_methods_are_also_availble_in_the_object + def test_module_methods_are_also_available_in_the_object fido = Dog.new assert_nothing_raised(Exception) do - fido.set_name("Rover") + fido.set_name("Rover") end end diff --git a/koans/about_nil.rb b/koans/about_nil.rb index b007cc0c2..5e1e28bfa 100644 --- a/koans/about_nil.rb +++ b/koans/about_nil.rb @@ -1,32 +1,24 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutNil < EdgeCase::Koan def test_nil_is_an_object - assert nil.is_a?(Object), "Unlike NULL in other languages" + assert_equal __, nil.is_a?(Object), "Unlike NULL in other languages" end def test_you_dont_get_null_pointer_errors_when_calling_methods_on_nil - # - # What is the Exception that is thrown when you call a method that - # does not exist? - # - # Hint: launch irb and try the code in the block below. - # - # Don't be confused by the code below yet. It's using blocks - # which are explained later on in about_blocks.rb. For now, - # think about it like running nil.some_method_nil_doesnt_know_about - # in a sandbox and catching the error class into the exception - # variable. - # - exception = assert_raise(___) do + # What happens when you call a method that doesn't exist. The + # following begin/rescue/end code block captures the exception and + # make some assertions about it. + begin nil.some_method_nil_doesnt_know_about + rescue Exception => ex + # What exception has been caught? + assert_equal __, ex.class + + # What message was attached to the exception? + # (HINT: replace __ with part of the error message.) + assert_match(/__/, ex.message) end - - # - # What is the error message itself? What substring or pattern could - # you test against in order to have a good idea what the string is? - # - assert_match /__/, exception.message end def test_nil_has_a_few_methods_defined_on_it diff --git a/koans/about_objects.rb b/koans/about_objects.rb new file mode 100644 index 000000000..05c44de0c --- /dev/null +++ b/koans/about_objects.rb @@ -0,0 +1,56 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutObjects < EdgeCase::Koan + def test_everything_is_an_object + assert_equal __, 1.is_a?(Object) + assert_equal __, 1.5.is_a?(Object) + assert_equal __, "string".is_a?(Object) + assert_equal __, nil.is_a?(Object) + assert_equal __, Object.is_a?(Object) + end + + def test_objects_can_be_converted_to_strings + assert_equal __, 123.to_s + assert_equal __, nil.to_s + end + + def test_objects_can_be_inspected + assert_equal __, 123.inspect + assert_equal __, nil.inspect + end + + def test_every_object_has_an_id + obj = Object.new + assert_equal __, obj.object_id.class + end + + def test_every_object_has_different_id + obj = Object.new + another_obj = Object.new + assert_equal __, obj.object_id != another_obj.object_id + end + + def test_some_system_objects_always_have_the_same_id + assert_equal __, false.object_id + assert_equal __, true.object_id + assert_equal __, nil.object_id + end + + def test_small_integers_have_fixed_ids + assert_equal __, 0.object_id + assert_equal __, 1.object_id + assert_equal __, 2.object_id + assert_equal __, 100.object_id + + # THINK ABOUT IT: + # What pattern do the object IDs for small integers follow? + end + + def test_clone_creates_a_different_object + obj = Object.new + copy = obj.clone + + assert_equal __, obj != copy + assert_equal __, obj.object_id != copy.object_id + end +end diff --git a/koans/about_open_classes.rb b/koans/about_open_classes.rb index 233cef507..afef1f956 100644 --- a/koans/about_open_classes.rb +++ b/koans/about_open_classes.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutOpenClasses < EdgeCase::Koan class Dog diff --git a/koans/about_proxy_object_project.rb b/koans/about_proxy_object_project.rb index dad63d7a2..1666e4596 100644 --- a/koans/about_proxy_object_project.rb +++ b/koans/about_proxy_object_project.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') # Project: Create a Proxy Class # @@ -6,7 +6,7 @@ # below). You should be able to initialize the proxy object with any # object. Any messages sent to the proxy object should be forwarded # to the target object. As each message is sent, the proxy should -# record the name of the method send. +# record the name of the method sent. # # The proxy class is started for you. You will need to add a method # missing handler and any other supporting methods. The specification @@ -15,7 +15,10 @@ class Proxy def initialize(target_object) @object = target_object + # ADD MORE CODE HERE end + + # WRITE CODE HERE end # The proxy object should pass the following Koan: @@ -30,7 +33,8 @@ def test_proxy_method_returns_wrapped_object def test_tv_methods_still_perform_their_function tv = Proxy.new(Television.new) - + + # HINT Proxy class is defined above, may need tweaking... tv.channel = 10 tv.power diff --git a/koans/about_regular_expressions.rb b/koans/about_regular_expressions.rb new file mode 100644 index 000000000..83449112c --- /dev/null +++ b/koans/about_regular_expressions.rb @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutRegularExpressions < EdgeCase::Koan + def test_a_pattern_is_a_regular_expression + assert_equal __, /pattern/.class + end + + def test_a_regexp_can_search_a_string_for_matching_content + assert_equal __, "some matching content"[/match/] + end + + def test_a_failed_match_returns_nil + assert_equal __, "some matching content"[/missing/] + end + + # ------------------------------------------------------------------ + + def test_question_mark_means_optional + assert_equal __, "abbcccddddeeeee"[/ab?/] + assert_equal __, "abbcccddddeeeee"[/az?/] + end + + def test_plus_means_one_or_more + assert_equal __, "abbcccddddeeeee"[/bc+/] + end + + def test_asterisk_means_zero_or_more + assert_equal __, "abbcccddddeeeee"[/ab*/] + assert_equal __, "abbcccddddeeeee"[/az*/] + assert_equal __, "abbcccddddeeeee"[/z*/] + + # THINK ABOUT IT: + # + # When would * fail to match? + end + + # THINK ABOUT IT: + # + # We say that the repetition operators above are "greedy." + # + # Why? + + # ------------------------------------------------------------------ + + def test_the_left_most_match_wins + assert_equal __, "abbccc az"[/az*/] + end + + # ------------------------------------------------------------------ + + def test_character_classes_give_options_for_a_character + animals = ["cat", "bat", "rat", "zat"] + assert_equal __, animals.select { |a| a[/[cbr]at/] } + end + + def test_slash_d_is_a_shortcut_for_a_digit_character_class + assert_equal __, "the number is 42"[/[0123456789]+/] + assert_equal __, "the number is 42"[/\d+/] + end + + def test_character_classes_can_include_ranges + assert_equal __, "the number is 42"[/[0-9]+/] + end + + def test_slash_s_is_a_shortcut_for_a_whitespace_character_class + assert_equal __, "space: \t\n"[/\s+/] + end + + def test_slash_w_is_a_shortcut_for_a_word_character_class + # NOTE: This is more like how a programmer might define a word. + assert_equal __, "variable_1 = 42"[/[a-zA-Z0-9_]+/] + assert_equal __, "variable_1 = 42"[/\w+/] + end + + def test_period_is_a_shortcut_for_any_non_newline_character + assert_equal __, "abc\n123"[/a.+/] + end + + def test_a_character_class_can_be_negated + assert_equal __, "the number is 42"[/[^0-9]+/] + end + + def test_shortcut_character_classes_are_negated_with_capitals + assert_equal __, "the number is 42"[/\D+/] + assert_equal __, "space: \t\n"[/\S+/] + assert_equal __, "variable_1 = 42"[/\W+/] + end + + # ------------------------------------------------------------------ + + def test_slash_a_anchors_to_the_start_of_the_string + assert_equal __, "start end"[/\Astart/] + assert_equal __, "start end"[/\Aend/] + end + + def test_slash_z_anchors_to_the_end_of_the_string + assert_equal __, "start end"[/end\z/] + assert_equal __, "start end"[/start\z/] + end + + def test_caret_anchors_to_the_start_of_lines + assert_equal __, "num 42\n2 lines"[/^\d+/] + end + + def test_dollar_sign_anchors_to_the_end_of_lines + assert_equal __, "2 lines\nnum 42"[/\d+$/] + end + + def test_slash_b_anchors_to_a_word_boundary + assert_equal __, "bovine vines"[/\bvine./] + end + + # ------------------------------------------------------------------ + + def test_parentheses_group_contents + assert_equal __, "ahahaha"[/(ha)+/] + end + + # ------------------------------------------------------------------ + + def test_parentheses_also_capture_matched_content_by_number + assert_equal __, "Gray, James"[/(\w+), (\w+)/, 1] + assert_equal __, "Gray, James"[/(\w+), (\w+)/, 2] + end + + def test_variables_can_also_be_used_to_access_captures + assert_equal __, "Name: Gray, James"[/(\w+), (\w+)/] + assert_equal __, $1 + assert_equal __, $2 + end + + # ------------------------------------------------------------------ + + def test_a_vertical_pipe_means_or + grays = /(James|Dana|Summer) Gray/ + assert_equal __, "James Gray"[grays] + assert_equal __, "Summer Gray"[grays, 1] + assert_equal __, "Jim Gray"[grays, 1] + end + + # THINK ABOUT IT: + # + # Explain the difference between a character class ([...]) and alternation (|). + + # ------------------------------------------------------------------ + + def test_scan_is_like_find_all + assert_equal __, "one two-three".scan(/\w+/) + end + + def test_sub_is_like_find_and_replace + assert_equal __, "one two-three".sub(/(t\w*)/) { $1[0, 1] } + end + + def test_gsub_is_like_find_and_replace_all + assert_equal __, "one two-three".gsub(/(t\w*)/) { $1[0, 1] } + end +end diff --git a/koans/about_sandwich_code.rb b/koans/about_sandwich_code.rb index 37f0c5fee..614a3740b 100644 --- a/koans/about_sandwich_code.rb +++ b/koans/about_sandwich_code.rb @@ -1,6 +1,6 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') -class AboutUsingBlocks < EdgeCase::Koan +class AboutSandwichCode < EdgeCase::Koan def count_lines(file_name) file = open(file_name) @@ -86,7 +86,7 @@ def find_line2(file_name) def test_finding_lines2 assert_equal __, find_line2("example_file.txt") end - + # ------------------------------------------------------------------ def count_lines3(file_name) diff --git a/koans/about_scope.rb b/koans/about_scope.rb index 968c360cf..d07d2af18 100644 --- a/koans/about_scope.rb +++ b/koans/about_scope.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutScope < EdgeCase::Koan module Jims @@ -28,9 +28,9 @@ def test_you_can_reference_nested_classes_using_the_scope_operator rover = Joes::Dog.new assert_equal __, fido.identify assert_equal __, rover.identify - - assert_not_equal fido.class, rover.class - assert_not_equal Jims::Dog, Joes::Dog + + assert_equal __, fido.class != rover.class + assert_equal __, Jims::Dog != Joes::Dog end # ------------------------------------------------------------------ @@ -41,7 +41,7 @@ class String def test_bare_bones_class_names_assume_the_current_scope assert_equal __, AboutScope::String == String end - + def test_nested_string_is_not_the_same_as_the_system_string assert_equal __, String == "HI".class end @@ -74,6 +74,6 @@ def test_constants_can_be_looked_up_explicitly def test_you_can_get_a_list_of_constants_for_any_class_or_module assert_equal __, Jims.constants - assert_equal __, Object.constants.size + assert Object.constants.size > _n_ end end diff --git a/koans/about_scoring_project.rb b/koans/about_scoring_project.rb index 69aeae010..bc617854e 100644 --- a/koans/about_scoring_project.rb +++ b/koans/about_scoring_project.rb @@ -1,13 +1,13 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') # Greed is a dice game where you roll up to five dice to accumulate -# points. The following "score" function will be used calculate the +# points. The following "score" function will be used to calculate the # score of a single roll of the dice. # # A greed roll is scored as follows: # # * A set of three ones is 1000 points -# +# # * A set of three numbers (other than ones) is worth 100 times the # number. (e.g. three fives is 500 points). # @@ -25,7 +25,7 @@ # score([3,4,5,3,3]) => 350 points # score([1,5,1,2,4]) => 250 points # -# More scoing examples are given in the tests below: +# More scoring examples are given in the tests below: # # Your goal is to write the score method. @@ -33,7 +33,7 @@ def score(dice) # You need to write this method end -class AboutScoringAssignment < EdgeCase::Koan +class AboutScoringProject < EdgeCase::Koan def test_score_of_an_empty_list_is_zero assert_equal 0, score([]) end @@ -46,8 +46,8 @@ def test_score_of_a_single_roll_of_1_is_100 assert_equal 100, score([1]) end - def test_score_of_mulitple_1s_and_5s_is_the_sum - assert_equal 200, score([1,5,5,1]) + def test_score_of_multiple_1s_and_5s_is_the_sum_of_individual_scores + assert_equal 300, score([1,5,5,1]) end def test_score_of_single_2s_3s_4s_and_6s_are_zero diff --git a/koans/about_strings.rb b/koans/about_strings.rb index db3289aad..aa427d48f 100644 --- a/koans/about_strings.rb +++ b/koans/about_strings.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutStrings < EdgeCase::Koan def test_double_quoted_strings_are_strings @@ -40,7 +40,8 @@ def test_flexible_quotes_can_handle_multiple_lines It was the best of times, It was the worst of times. } - assert_equal __, long_string.size + assert_equal __, long_string.length + assert_equal __, long_string.lines.count end def test_here_documents_can_also_handle_multiple_lines @@ -48,7 +49,8 @@ def test_here_documents_can_also_handle_multiple_lines It was the best of times, It was the worst of times. EOS - assert_equal __, long_string.size + assert_equal __, long_string.length + assert_equal __, long_string.lines.count end def test_plus_will_concatenate_two_strings @@ -128,7 +130,7 @@ def test_single_quoted_strings_do_not_interpolate assert_equal __, string end - def test_any_ruby_expression_my_be_interpolated + def test_any_ruby_expression_may_be_interpolated string = "The square root of 5 is #{Math.sqrt(5)}" assert_equal __, string end @@ -139,20 +141,42 @@ def test_you_can_get_a_substring_from_a_string assert_equal __, string[7..9] end - def test_you_can_get_a_single_character_from_a_string - string = "Bacon, lettuce and tomato" - assert_equal __, string[1] + in_ruby_version("1.8") do + def test_in_ruby_1_8_single_characters_are_represented_by_integers + assert_equal __, ?a + assert_equal __, ?a == 97 + + assert_equal __, ?b == (?a + 1) + end + end + + in_ruby_version("1.9") do + def test_in_ruby_1_9_single_characters_are_represented_by_strings + assert_equal __, ?a + assert_equal __, ?a == 97 + end + end - # Surprised? +in_ruby_version("1.8") do + def test_in_ruby_1_8_you_can_get_a_single_character_from_a_string + string = "Bacon, lettuce and tomato" + assert_equal __, string[1] + + # Surprised? + end end + + in_ruby_version("1.9") do - def test_single_characters_are_represented_by_integers - assert_equal __, ?a - assert_equal __, ?a == 97 + def test_in_ruby_1_9_you_can_get_a_single_character_from_a_string + string = "Bacon, lettuce and tomato" + assert_equal "__", string[1] - assert_equal __, ?b == (?a + 1) + # Surprised? + end end + def test_strings_can_be_split string = "Sausage Egg Cheese" words = string.split @@ -165,12 +189,20 @@ def test_strings_can_be_split_with_different_patterns assert_equal [__, __, __, __], words # NOTE: Patterns are formed from Regular Expressions. Ruby has a - # very powerful Regular Expression library. Unfortunately, time - # does not permit us to explore it in detail in Ruby 101. + # very powerful Regular Expression library. We will become + # enlightened about them soon. end def test_strings_can_be_joined words = ["Now", "is", "the", "time"] assert_equal __, words.join(" ") end + + def test_strings_are_unique_objects + a = "a string" + b = "a string" + + assert_equal __, a == b + assert_equal __, a.object_id == b.object_id + end end diff --git a/koans/about_symbols.rb b/koans/about_symbols.rb new file mode 100644 index 000000000..f4a431968 --- /dev/null +++ b/koans/about_symbols.rb @@ -0,0 +1,100 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutSymbols < EdgeCase::Koan + def test_symbols_are_symbols + symbol = :ruby + assert_equal __, symbol.is_a?(Symbol) + end + + def test_symbols_can_be_compared + symbol1 = :a_symbol + symbol2 = :a_symbol + symbol3 = :something_else + + assert_equal __, symbol1 == symbol2 + assert_equal __, symbol1 == symbol3 + end + + def test_identical_symbols_are_a_single_internal_object + symbol1 = :a_symbol + symbol2 = :a_symbol + + assert_equal __, symbol1 == symbol2 + assert_equal __, symbol1.object_id == symbol2.object_id + end + + def test_method_names_become_symbols + symbols_as_strings = Symbol.all_symbols.map { |x| x.to_s } + assert_equal __, symbols_as_strings.include?("test_method_names_become_symbols") + end + + # THINK ABOUT IT: + # + # Why do we convert the list of symbols to strings and then compare + # against the string value rather than against symbols? + + in_ruby_version("mri") do + RubyConstant = "What is the sound of one hand clapping?" + def test_constants_become_symbols + all_symbols = Symbol.all_symbols + + assert_equal __, all_symbols.include?(__) + end + end + + def test_symbols_can_be_made_from_strings + string = "catsAndDogs" + assert_equal __, string.to_sym + end + + def test_symbols_with_spaces_can_be_built + symbol = :"cats and dogs" + + assert_equal symbol, __.to_sym + end + + def test_symbols_with_interpolation_can_be_built + value = "and" + symbol = :"cats #{value} dogs" + + assert_equal symbol, __.to_sym + end + + def test_to_s_is_called_on_interpolated_symbols + symbol = :cats + string = "It is raining #{symbol} and dogs." + + assert_equal __, string + end + + def test_symbols_are_not_strings + symbol = :ruby + assert_equal __, symbol.is_a?(String) + assert_equal __, symbol.eql?("ruby") + end + + def test_symbols_do_not_have_string_methods + symbol = :not_a_string + assert_equal __, symbol.respond_to?(:each_char) + assert_equal __, symbol.respond_to?(:reverse) + end + + # It's important to realize that symbols are not "immutable + # strings", though they are immutable. None of the + # interesting string operations are available on symbols. + + def test_symbols_cannot_be_concatenated + # Exceptions will be pondered further farther down the path + assert_raise(___) do + :cats + :dogs + end + end + + def test_symbols_can_be_dynamically_created + assert_equal __, ("cats" + "dogs").to_sym + end + + # THINK ABOUT IT: + # + # Why is it not a good idea to dynamically create a lot of symbols? +end diff --git a/koans/about_to_str.rb b/koans/about_to_str.rb new file mode 100644 index 000000000..964850dc2 --- /dev/null +++ b/koans/about_to_str.rb @@ -0,0 +1,54 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutToStr < EdgeCase::Koan + + class CanNotBeTreatedAsString + def to_s + "non-string-like" + end + end + + def test_to_s_returns_a_string_representation + not_like_a_string = CanNotBeTreatedAsString.new + assert_equal __, not_like_a_string.to_s + end + + def test_normally_objects_cannot_be_used_where_strings_are_expected + assert_raise(___) do + File.exist?(CanNotBeTreatedAsString.new) + end + end + + # ------------------------------------------------------------------ + + class CanBeTreatedAsString + def to_s + "string-like" + end + + def to_str + to_s + end + end + + def test_to_str_also_returns_a_string_representation + like_a_string = CanBeTreatedAsString.new + assert_equal __, like_a_string.to_str + end + + def test_to_str_allows_objects_to_be_treated_as_strings + assert_equal __, File.exist?(CanBeTreatedAsString.new) + end + + # ------------------------------------------------------------------ + + def acts_like_a_string?(string) + string = string.to_str if string.respond_to?(:to_str) + string.is_a?(String) + end + + def test_user_defined_code_can_check_for_to_str + assert_equal __, acts_like_a_string?(CanNotBeTreatedAsString.new) + assert_equal __, acts_like_a_string?(CanBeTreatedAsString.new) + end +end diff --git a/koans/about_triangle_project.rb b/koans/about_triangle_project.rb index 5c1855cf1..da23bbd78 100644 --- a/koans/about_triangle_project.rb +++ b/koans/about_triangle_project.rb @@ -1,9 +1,9 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') # You need to write the triangle method in the file 'triangle.rb' require 'triangle.rb' -class AboutTriangleAssignment < EdgeCase::Koan +class AboutTriangleProject < EdgeCase::Koan def test_equilateral_triangles_have_equal_sides assert_equal :equilateral, triangle(2, 2, 2) assert_equal :equilateral, triangle(10, 10, 10) @@ -22,4 +22,4 @@ def test_scalene_triangles_have_no_equal_sides assert_equal :scalene, triangle(5, 4, 2) end end - + diff --git a/koans/about_triangle_project_2.rb b/koans/about_triangle_project_2.rb index f9e472815..fc90ba181 100644 --- a/koans/about_triangle_project_2.rb +++ b/koans/about_triangle_project_2.rb @@ -1,15 +1,17 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') # You need to write the triangle method in the file 'triangle.rb' require 'triangle.rb' -class AboutTriangleAssignment < EdgeCase::Koan +class AboutTriangleProject2 < EdgeCase::Koan # The first assignment did not talk about how to handle errors. # Let's handle that part now. def test_illegal_triangles_throw_exceptions assert_raise(TriangleError) do triangle(0, 0, 0) end assert_raise(TriangleError) do triangle(3, 4, -5) end - assert_raise(TriangleError) do triangle(2, 4, 2) end + assert_raise(TriangleError) do triangle(1, 1, 3) end + assert_raise(TriangleError) do triangle(2, 4, 2) end + #HINT: for tips, see http://stackoverflow.com/questions/3834203/ruby-koan-151-raising-exceptions end end - + diff --git a/koans/about_true_and_false.rb b/koans/about_true_and_false.rb index 51922516e..1138fb96e 100644 --- a/koans/about_true_and_false.rb +++ b/koans/about_true_and_false.rb @@ -1,4 +1,4 @@ -require 'edgecase' +require File.expand_path(File.dirname(__FILE__) + '/edgecase') class AboutTrueAndFalse < EdgeCase::Koan def truth_value(condition) @@ -10,7 +10,7 @@ def truth_value(condition) end def test_true_is_treated_as_true - assert_equal __, truth_value(true) + assert_equal __, truth_value(true) end def test_false_is_treated_as_false diff --git a/koans/autotest/discover.rb b/koans/autotest/discover.rb new file mode 100644 index 000000000..31a7804b2 --- /dev/null +++ b/koans/autotest/discover.rb @@ -0,0 +1,3 @@ +Autotest.add_discovery do + "rubykoan" if File.exist? 'path_to_enlightenment.rb' +end diff --git a/koans/autotest/rubykoan.rb b/koans/autotest/rubykoan.rb new file mode 100644 index 000000000..d43dc9112 --- /dev/null +++ b/koans/autotest/rubykoan.rb @@ -0,0 +1,24 @@ +require 'autotest' + +class Autotest::Rubykoan < Autotest + def initialize + super + @exceptions = /\.txt|Rakefile|\.rdoc/ + + self.order = :alpha + self.add_mapping(/^about_.*rb$/) do |filename, _| + filename + end + + end + + def make_test_cmd files_to_test + "#{ruby} 'path_to_enlightenment.rb'" + end + + # quiet test/unit chatter + def handle_results(results) + end + +end + diff --git a/koans/code_mash.rb b/koans/code_mash.rb index fe089a5ef..8fbf61788 100644 --- a/koans/code_mash.rb +++ b/koans/code_mash.rb @@ -1 +1 @@ -require 'edgecase' \ No newline at end of file +require File.expand_path(File.dirname(__FILE__) + '/edgecase') diff --git a/koans/edgecase.rb b/koans/edgecase.rb index 7ab429e9d..ba49956de 100644 --- a/koans/edgecase.rb +++ b/koans/edgecase.rb @@ -3,37 +3,177 @@ require 'test/unit/assertions' +# -------------------------------------------------------------------- +# Support code for the Ruby Koans. +# -------------------------------------------------------------------- + class FillMeInError < StandardError end -def __(value="FILL ME IN") - value +def ruby_version?(version) + RUBY_VERSION =~ /^#{version}/ || + (version == 'jruby' && defined?(JRUBY_VERSION)) || + (version == 'mri' && ! defined?(JRUBY_VERSION)) +end + +def in_ruby_version(*versions) + yield if versions.any? { |v| ruby_version?(v) } end +# Standard, generic replacement value. +# If value19 is given, it is used in place of value for Ruby 1.9. +def __(value="FILL ME IN", value19=:mu) + if RUBY_VERSION < "1.9" + value + else + (value19 == :mu) ? value : value19 + end +end + +# Numeric replacement value. +def _n_(value=999999, value19=:mu) + if RUBY_VERSION < "1.9" + value + else + (value19 == :mu) ? value : value19 + end +end + +# Error object replacement value. def ___(value=FillMeInError) value end +# Method name replacement. +class Object + def ____(method=nil) + if method + self.send(method) + end + end + + in_ruby_version("1.9") do + public :method_missing + end +end + +class String + def side_padding(width) + extra = width - self.size + if width < 0 + self + else + left_padding = extra / 2 + right_padding = (extra+1)/2 + (" " * left_padding) + self + (" " *right_padding) + end + end +end + module EdgeCase + class << self + def simple_output + ENV['SIMPLE_KOAN_OUTPUT'] == 'true' + end + end + + module Color + #shamelessly stolen (and modified) from redgreen + COLORS = { + :clear => 0, :black => 30, :red => 31, + :green => 32, :yellow => 33, :blue => 34, + :magenta => 35, :cyan => 36, + } + + module_function + + COLORS.each do |color, value| + module_eval "def #{color}(string); colorize(string, #{value}); end" + module_function color + end + + def colorize(string, color_value) + if use_colors? + color(color_value) + string + color(COLORS[:clear]) + else + string + end + end + + def color(color_value) + "\e[#{color_value}m" + end + + def use_colors? + return false if ENV['NO_COLOR'] + if ENV['ANSI_COLOR'].nil? + ! using_windows? + else + ENV['ANSI_COLOR'] =~ /^(t|y)/i + end + end + + def using_windows? + File::ALT_SEPARATOR + end + end + class Sensei - attr_reader :failure, :failed_test + attr_reader :failure, :failed_test, :pass_count - AssertionError = Test::Unit::AssertionFailedError + in_ruby_version("1.8") do + AssertionError = Test::Unit::AssertionFailedError + end + + in_ruby_version("1.9") do + if defined?(MiniTest) + AssertionError = MiniTest::Assertion + else + AssertionError = Test::Unit::AssertionFailedError + end + end def initialize @pass_count = 0 @failure = nil @failed_test = nil + @observations = [] + end + + PROGRESS_FILE_NAME = '.path_progress' + + def add_progress(prog) + @_contents = nil + exists = File.exists?(PROGRESS_FILE_NAME) + File.open(PROGRESS_FILE_NAME,'a+') do |f| + f.print "#{',' if exists}#{prog}" + end + end + + def progress + if @_contents.nil? + if File.exists?(PROGRESS_FILE_NAME) + File.open(PROGRESS_FILE_NAME,'r') do |f| + @_contents = f.read.to_s.gsub(/\s/,'').split(',') + end + else + @_contents = [] + end + end + @_contents end - def accumulate(test) - if test.passed? + def observe(step) + if step.passed? @pass_count += 1 - puts " #{test.name} has expanded your awareness." + if @pass_count > progress.last.to_i + @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.") + end else - puts " #{test.name} has damaged your karma." - @failed_test = test - @failure = test.failure + @failed_test = step + @failure = step.failure + add_progress(@pass_count) + @observations << Color.red("#{step.koan_file}##{step.name} has damaged your karma.") throw :edgecase_exit end end @@ -46,62 +186,175 @@ def assert_failed? failure.is_a?(AssertionError) end - def report + def instruct + if failed? + @observations.each{|c| puts c } + encourage + guide_through_error + a_zenlike_statement + show_progress + else + end_screen + end + end + + def show_progress + bar_width = 50 + total_tests = EdgeCase::Koan.total_tests + scale = bar_width.to_f/total_tests + print Color.green("your path thus far [") + happy_steps = (pass_count*scale).to_i + happy_steps = 1 if happy_steps == 0 && pass_count > 0 + print Color.green('.'*happy_steps) if failed? - puts - puts "You have not yet reached enlightenment ..." - puts failure.message - puts - puts "Please meditate on the following code:" - if assert_failed? - puts find_interesting_lines(failure.backtrace) + print Color.red('X') + print Color.cyan('_'*(bar_width-1-happy_steps)) + end + print Color.green(']') + print " #{pass_count}/#{total_tests}" + puts + end + + def end_screen + if EdgeCase.simple_output + boring_end_screen + else + artistic_end_screen + end + end + + def boring_end_screen + puts "Mountains are again merely mountains" + end + + def artistic_end_screen + "JRuby 1.9.x Koans" + ruby_version = "(in #{'J' if defined?(JRUBY_VERSION)}Ruby #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION})" + ruby_version = ruby_version.side_padding(54) + completed = <<-ENDTEXT + ,, , ,, + : ::::, :::, + , ,,: :::::::::::::,, :::: : , + , ,,, ,:::::::::::::::::::, ,: ,: ,, + :, ::, , , :, ,::::::::::::::::::, ::: ,:::: + : : ::, ,:::::::: ::, ,:::: + , ,::::: :,:::::::,::::, + ,: , ,:,,: ::::::::::::: + ::,: ,,:::, ,::::::::::::, + ,:::, :,,::: ::::::::::::, + ,::: :::::::, Mountains are again merely mountains ,:::::::::::: + :::,,,:::::: :::::::::::: + ,:::::::::::, ::::::::::::, + :::::::::::, ,:::::::::::: +::::::::::::: ,:::::::::::: +:::::::::::: Ruby Koans ::::::::::::, +::::::::::::#{ ruby_version },::::::::::::, +:::::::::::, , :::::::::::: +,:::::::::::::, brought to you by ,,::::::::::::, +:::::::::::::: ,:::::::::::: + ::::::::::::::, ,::::::::::::: + ::::::::::::, EdgeCase Software Artisans , :::::::::::: + :,::::::::: :::: ::::::::::::: + ,::::::::::: ,: ,,:::::::::::::, + :::::::::::: ,::::::::::::::, + :::::::::::::::::, :::::::::::::::: + :::::::::::::::::::, :::::::::::::::: + ::::::::::::::::::::::, ,::::,:, , ::::,::: + :::::::::::::::::::::::, ::,: ::,::, ,,: :::: + ,:::::::::::::::::::: ::,, , ,, ,:::: + ,:::::::::::::::: ::,, , ,:::, + ,:::: , ,, + ,,, +ENDTEXT + puts completed + end + + def encourage + puts + puts "The Master says:" + puts Color.cyan(" You have not yet reached enlightenment.") + if ((recents = progress.last(5)) && recents.size == 5 && recents.uniq.size == 1) + puts Color.cyan(" I sense frustration. Do not be afraid to ask for help.") + elsif progress.last(2).size == 2 && progress.last(2).uniq.size == 1 + puts Color.cyan(" Do not lose hope.") + elsif progress.last.to_i > 0 + puts Color.cyan(" You are progressing. Excellent. #{progress.last} completed.") + end + end + + def guide_through_error + puts + puts "The answers you seek..." + puts Color.red(indent(failure.message).join) + puts + puts "Please meditate on the following code:" + if assert_failed? + puts embolden_first_line_only(indent(find_interesting_lines(failure.backtrace))) + else + puts embolden_first_line_only(indent(failure.backtrace)) + end + puts + end + + def embolden_first_line_only(text) + first_line = true + text.collect { |t| + if first_line + first_line = false + Color.red(t) else - puts failure.backtrace + Color.cyan(t) end - puts - end - say_something_zenlike + } + end + + def indent(text) + text = text.split(/\n/) if text.is_a?(String) + text.collect{|t| " #{t}"} end def find_interesting_lines(backtrace) backtrace.reject { |line| - line =~ /test\/unit\/|edgecase\.rb/ + line =~ /test\/unit\/|edgecase\.rb|minitest/ } end # Hat's tip to Ara T. Howard for the zen statements from his # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html) - def say_something_zenlike - puts + def a_zenlike_statement if !failed? - puts "Mountains are again merely mountains" + zen_statement = "Mountains are again merely mountains" else - case (@pass_count % 10) + zen_statement = case (@pass_count % 10) when 0 - puts "mountains are merely mountains" + "mountains are merely mountains" when 1, 2 - puts "learn the rules so you know how to break them properly" + "learn the rules so you know how to break them properly" when 3, 4 - puts "remember that silence is sometimes the best answer" + "remember that silence is sometimes the best answer" when 5, 6 - puts "sleep is the best meditation" + "sleep is the best meditation" when 7, 8 - puts "when you lose, don't lose the lesson" + "when you lose, don't lose the lesson" else - puts "things are not what they appear to be: nor are they otherwise" + "things are not what they appear to be: nor are they otherwise" end end + puts Color.green(zen_statement) end - end + end class Koan include Test::Unit::Assertions - attr_reader :name, :failure + attr_reader :name, :failure, :koan_count, :step_count, :koan_file - def initialize(name) + def initialize(name, koan_file=nil, koan_count=0, step_count=0) @name = name @failure = nil + @koan_count = koan_count + @step_count = step_count + @koan_file = koan_file end def passed? @@ -118,6 +371,22 @@ def setup def teardown end + def meditate + setup + begin + send(name) + rescue StandardError, EdgeCase::Sensei::AssertionError => ex + failed(ex) + ensure + begin + teardown + rescue StandardError, EdgeCase::Sensei::AssertionError => ex + failed(ex) if passed? + end + end + self + end + # Class methods for the EdgeCase test suite. class << self def inherited(subclass) @@ -125,32 +394,7 @@ def inherited(subclass) end def method_added(name) - testmethods << name unless tests_disabled? - end - - def run_tests(accumulator) - puts - puts "Thinking #{self}" - testmethods.each do |m| - self.run_test(m, accumulator) if Koan.test_pattern =~ m.to_s - end - end - - def run_test(method, accumulator) - test = self.new(method) - test.setup - begin - test.send(method) - rescue StandardError => ex - test.failed(ex) - ensure - begin - test.teardown - rescue StandardError => ex - test.failed(ex) if test.passed? - end - end - accumulator.accumulate(test) + testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s end def end_of_enlightenment @@ -169,7 +413,7 @@ def command_line(args) load(arg) else fail "Unknown command line argument '#{arg}'" - end + end end end end @@ -192,17 +436,36 @@ def test_pattern @test_pattern ||= /^test_/ end + def total_tests + self.subclasses.inject(0){|total, k| total + k.testmethods.size } + end + end + end + + class ThePath + def walk + sensei = EdgeCase::Sensei.new + each_step do |step| + sensei.observe(step.meditate) + end + sensei.instruct + end + + def each_step + catch(:edgecase_exit) { + step_count = 0 + EdgeCase::Koan.subclasses.each_with_index do |koan,koan_index| + koan.testmethods.each do |method_name| + step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1) + yield step + end + end + } end end end END { EdgeCase::Koan.command_line(ARGV) - zen_master = EdgeCase::Sensei.new - catch(:edgecase_exit) { - EdgeCase::Koan.subclasses.each do |sc| - sc.run_tests(zen_master) - end - } - zen_master.report + EdgeCase::ThePath.new.walk } diff --git a/koans/path_to_enlightenment.rb b/koans/path_to_enlightenment.rb index f8e323f95..64621bf4f 100644 --- a/koans/path_to_enlightenment.rb +++ b/koans/path_to_enlightenment.rb @@ -1,12 +1,18 @@ # The path to Ruby Enlightenment starts with the following: -require 'about_basics' +$LOAD_PATH << File.dirname(__FILE__) + +require 'about_asserts' require 'about_nil' +require 'about_objects' require 'about_arrays' require 'about_array_assignment' require 'about_hashes' require 'about_strings' +require 'about_symbols' +require 'about_regular_expressions' require 'about_methods' +require 'about_constants' require 'about_control_statements' require 'about_true_and_false' require 'about_triangle_project' @@ -17,6 +23,7 @@ require 'about_sandwich_code' require 'about_scoring_project' require 'about_classes' +require 'about_open_classes' require 'about_dice_project' require 'about_inheritance' require 'about_modules' @@ -24,4 +31,8 @@ require 'about_class_methods' require 'about_message_passing' require 'about_proxy_object_project' +require 'about_to_str' +in_ruby_version("jruby") do + require 'about_java_interop' +end require 'about_extra_credit' diff --git a/rakelib/checks.rake b/rakelib/checks.rake new file mode 100644 index 000000000..fa5aaf7e0 --- /dev/null +++ b/rakelib/checks.rake @@ -0,0 +1,33 @@ +namespace "check" do + + desc "Check that the require files match the about_* files" + task :abouts do + about_files = Dir['src/about_*.rb'].size + about_requires = `grep require src/path_to_enlightenment.rb | wc -l`.to_i + puts "Checking path_to_enlightenment completeness" + puts "# of about files: #{about_files}" + puts "# of about requires: #{about_requires}" + if about_files > about_requires + puts "*** There seems to be requires missing in the path to enlightenment" + else + puts "OK" + end + puts + end + + desc "Check that asserts have __ replacements" + task :asserts do + puts "Checking for asserts missing the replacement text:" + begin + sh "egrep -n 'assert( |_)' src/about_* | egrep -v '__|_n_|project|about_assert' | egrep -v ' *#'" + puts + puts "Examine the above lines for missing __ replacements" + rescue RuntimeError => ex + puts "OK" + end + puts + end +end + +desc "Run some simple consistency checks" +task :check => ["check:abouts", "check:asserts"] diff --git a/rakelib/run.rake b/rakelib/run.rake new file mode 100644 index 000000000..a21917833 --- /dev/null +++ b/rakelib/run.rake @@ -0,0 +1,8 @@ +RUBIES = ENV['KOAN_RUBIES'] || %w(ruby-1.8.7-p299,ruby-1.9.2-p0,jruby-1.5.2,jruby-head) + +task :runall do + chdir('src') do + ENV['SIMPLE_KOAN_OUTPUT'] = 'true' + sh "rvm #{RUBIES} path_to_enlightenment.rb" + end +end diff --git a/src/GREED_RULES.txt b/src/GREED_RULES.txt new file mode 100644 index 000000000..58b5a9cb6 --- /dev/null +++ b/src/GREED_RULES.txt @@ -0,0 +1,66 @@ += Playing Greed + +Greed is a dice game played among 2 or more players, using 5 +six-sided dice. + +== Playing Greed + +Each player takes a turn consisting of one or more rolls of the dice. +On the first roll of the game, a player rolls all five dice which are +scored according to the following: + + Three 1's => 1000 points + Three 6's => 600 points + Three 5's => 500 points + Three 4's => 400 points + Three 3's => 300 points + Three 2's => 200 points + One 1 => 100 points + One 5 => 50 points + +A single die can only be counted once in each roll. For example, +a "5" can only count as part of a triplet (contributing to the 500 +points) or as a single 50 points, but not both in the same roll. + +Example Scoring + + Throw Score + --------- ------------------ + 5 1 3 4 1 50 + 2 * 100 = 250 + 1 1 1 3 1 1000 + 100 = 1100 + 2 4 4 5 4 400 + 50 = 450 + +The dice not contributing to the score are called the non-scoring +dice. "3" and "4" are non-scoring dice in the first example. "3" is +a non-scoring die in the second, and "2" is a non-score die in the +final example. + +After a player rolls and the score is calculated, the scoring dice are +removed and the player has the option of rolling again using only the +non-scoring dice. If all of the thrown dice are scoring, then the +player may roll all 5 dice in the next roll. + +The player may continue to roll as long as each roll scores points. If +a roll has zero points, then the player loses not only their turn, but +also accumulated score for that turn. If a player decides to stop +rolling before rolling a zero-point roll, then the accumulated points +for the turn is added to his total score. + +== Getting "In The Game" + +Before a player is allowed to accumulate points, they must get at +least 300 points in a single turn. Once they have achieved 300 points +in a single turn, the points earned in that turn and each following +turn will be counted toward their total score. + +== End Game + +Once a player reaches 3000 (or more) points, the game enters the final +round where each of the other players gets one more turn. The winner +is the player with the highest score after the final round. + +== References + +Greed is described on Wikipedia at +http://en.wikipedia.org/wiki/Greed_(dice_game), however the rules are +a bit different from the rules given here. diff --git a/src/Rakefile b/src/Rakefile new file mode 100644 index 000000000..1a2c7f26d --- /dev/null +++ b/src/Rakefile @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +# -*- ruby -*- + +require 'rake/clean' +require 'rake/testtask' + +task :default => :test + +task :test do + ruby 'path_to_enlightenment.rb' +end + diff --git a/src/about_array_assignment.rb b/src/about_array_assignment.rb new file mode 100644 index 000000000..35af856a2 --- /dev/null +++ b/src/about_array_assignment.rb @@ -0,0 +1,51 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutArrayAssignment < EdgeCase::Koan + def test_non_parallel_assignment + names = ["John", "Smith"] + assert_equal __(["John", "Smith"]), names + end + + def test_parallel_assignments + first_name, last_name = ["John", "Smith"] + assert_equal __("John"), first_name + assert_equal __("Smith"), last_name + end + + def test_parallel_assignments_with_extra_values + first_name, last_name = ["John", "Smith", "III"] + assert_equal __("John"), first_name + assert_equal __("Smith"), last_name + end + + def test_parallel_assignments_with_splat_operator + first_name, *last_name = ["John", "Smith", "III"] + assert_equal __("John"), first_name + assert_equal __(["Smith","III"]), last_name + end + + def test_parallel_assignments_with_too_few_variables + first_name, last_name = ["Cher"] + assert_equal __("Cher"), first_name + assert_equal __(nil), last_name + end + + def test_parallel_assignments_with_subarrays + first_name, last_name = [["Willie", "Rae"], "Johnson"] + assert_equal __(["Willie", "Rae"]), first_name + assert_equal __("Johnson"), last_name + end + + def test_parallel_assignment_with_one_variable + first_name, = ["John", "Smith"] + assert_equal __("John"), first_name + end + + def test_swapping_with_parallel_assignment + first_name = "Roy" + last_name = "Rob" + first_name, last_name = last_name, first_name + assert_equal __('Rob'), first_name + assert_equal __('Roy'), last_name + end +end diff --git a/src/about_arrays.rb b/src/about_arrays.rb new file mode 100644 index 000000000..35c951d64 --- /dev/null +++ b/src/about_arrays.rb @@ -0,0 +1,84 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutArrays < EdgeCase::Koan + def test_creating_arrays + empty_array = Array.new + assert_equal __(Array), empty_array.class + assert_equal __(0), empty_array.size + end + + def test_array_literals + array = Array.new + assert_equal [], array # __ + + array[0] = 1 + assert_equal [1], array # __ + + array[1] = 2 + assert_equal [1, __(2)], array + + array << 333 + assert_equal __([1, 2, 333]), array + end + + def test_accessing_array_elements + array = [:peanut, :butter, :and, :jelly] + + assert_equal __(:peanut), array[0] + assert_equal __(:peanut), array.first + assert_equal __(:jelly), array[3] + assert_equal __(:jelly), array.last + assert_equal __(:jelly), array[-1] + assert_equal __(:butter), array[-3] + end + + def test_slicing_arrays + array = [:peanut, :butter, :and, :jelly] + + assert_equal __([:peanut]), array[0,1] + assert_equal __([:peanut, :butter]), array[0,2] + assert_equal __([:and, :jelly]), array[2,2] + assert_equal __([:and, :jelly]), array[2,20] + assert_equal __([]), array[4,0] + assert_equal __([]), array[4,100] + assert_equal __(nil), array[5,0] + end + + def test_arrays_and_ranges + assert_equal __(Range), (1..5).class + assert_not_equal [1,2,3,4,5], (1..5) # __ + assert_equal __([1,2,3,4,5]), (1..5).to_a + assert_equal __([1,2,3,4]), (1...5).to_a + end + + def test_slicing_with_ranges + array = [:peanut, :butter, :and, :jelly] + + assert_equal __([:peanut, :butter, :and]), array[0..2] + assert_equal __([:peanut, :butter]), array[0...2] + assert_equal __([:and, :jelly]), array[2..-1] + end + + def test_pushing_and_popping_arrays + array = [1,2] + array.push(:last) + + assert_equal __([1, 2, :last]), array + + popped_value = array.pop + assert_equal __(:last), popped_value + assert_equal __([1, 2]), array + end + + def test_shifting_arrays + array = [1,2] + array.unshift(:first) + + assert_equal __([:first, 1, 2]), array + + shifted_value = array.shift + assert_equal __(:first), shifted_value + assert_equal __([1, 2]), array + end + +end diff --git a/src/about_asserts.rb b/src/about_asserts.rb new file mode 100644 index 000000000..5ac6b551f --- /dev/null +++ b/src/about_asserts.rb @@ -0,0 +1,54 @@ +#!/usr/bin/env ruby +# -*- ruby -*- + +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutAsserts < EdgeCase::Koan + + # We shall contemplate truth by testing reality, via asserts. + def test_assert_truth + #-- + assert true # This should be true + if false + #++ + assert false # This should be true + #-- + end + #++ + end + + # Enlightenment may be more easily achieved with appropriate + # messages. + def test_assert_with_message + #-- + assert true, "This should be true -- Please fix this" + if false + #++ + assert false, "This should be true -- Please fix this" + #-- + end + #++ + end + + # To understand reality, we must compare our expectations against + # reality. + def test_assert_equality + expected_value = __(2) + actual_value = 1 + 1 + + assert expected_value == actual_value + end + + # Some ways of asserting equality are better than others. + def test_a_better_way_of_asserting_equality + expected_value = __(2) + actual_value = 1 + 1 + + assert_equal expected_value, actual_value + end + + # Sometimes we will ask you to fill in the values + def test_fill_in_values + assert_equal __(2), 1 + 1 + end +end diff --git a/src/about_blocks.rb b/src/about_blocks.rb new file mode 100644 index 000000000..7fefcd907 --- /dev/null +++ b/src/about_blocks.rb @@ -0,0 +1,96 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutBlocks < EdgeCase::Koan + def method_with_block + result = yield + result + end + + def test_methods_can_take_blocks + yielded_result = method_with_block { 1 + 2 } + assert_equal __(3), yielded_result + end + + def test_blocks_can_be_defined_with_do_end_too + yielded_result = method_with_block do 1 + 2 end + assert_equal __(3), yielded_result + end + + # ------------------------------------------------------------------ + + def method_with_block_arguments + yield("Jim") + end + + def test_blocks_can_take_arguments + result = method_with_block_arguments do |argument| + assert_equal __("Jim"), argument + end + end + + # ------------------------------------------------------------------ + + def many_yields + yield(:peanut) + yield(:butter) + yield(:and) + yield(:jelly) + end + + def test_methods_can_call_yield_many_times + result = [] + many_yields { |item| result << item } + assert_equal __([:peanut, :butter, :and, :jelly]), result + end + + # ------------------------------------------------------------------ + + def yield_tester + if block_given? + yield + else + :no_block + end + end + + def test_methods_can_see_if_they_have_been_called_with_a_block + assert_equal __(:with_block), yield_tester { :with_block } + assert_equal __(:no_block), yield_tester + end + + # ------------------------------------------------------------------ + + def test_block_can_affect_variables_in_the_code_where_they_are_created + value = :initial_value + method_with_block { value = :modified_in_a_block } + assert_equal __(:modified_in_a_block), value + end + + def test_blocks_can_be_assigned_to_variables_and_called_explicitly + add_one = lambda { |n| n + 1 } + assert_equal __(11), add_one.call(10) + + # Alternative calling sequence + assert_equal __(11), add_one[10] + end + + def test_stand_alone_blocks_can_be_passed_to_methods_expecting_blocks + make_upper = lambda { |n| n.upcase } + result = method_with_block_arguments(&make_upper) + assert_equal __("JIM"), result + end + + # ------------------------------------------------------------------ + + def method_with_explicit_block(&block) + block.call(10) + end + + def test_methods_can_take_an_explicit_block_argument + assert_equal __(20), method_with_explicit_block { |n| n * 2 } + + add_one = lambda { |n| n + 1 } + assert_equal __(11), method_with_explicit_block(&add_one) + end + +end diff --git a/src/about_class_methods.rb b/src/about_class_methods.rb new file mode 100644 index 000000000..930345d6a --- /dev/null +++ b/src/about_class_methods.rb @@ -0,0 +1,170 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutClassMethods < EdgeCase::Koan + class Dog + end + + def test_objects_are_objects + fido = Dog.new + assert_equal __(true), fido.is_a?(Object) + end + + def test_classes_are_classes + assert_equal __(true), Dog.is_a?(Class) + end + + def test_classes_are_objects_too + assert_equal __(true), Dog.is_a?(Object) + end + + def test_objects_have_methods + fido = Dog.new + assert fido.methods.size > _n_(30) + end + + def test_classes_have_methods + assert Dog.methods.size > _n_(40) + end + + def test_you_can_define_methods_on_individual_objects + fido = Dog.new + def fido.wag + :fidos_wag + end + assert_equal __(:fidos_wag), fido.wag + end + + def test_other_objects_are_not_affected_by_these_singleton_methods + fido = Dog.new + rover = Dog.new + def fido.wag + :fidos_wag + end + + assert_raise(___(NoMethodError)) do + rover.wag + end + end + + # ------------------------------------------------------------------ + + class Dog2 + def wag + :instance_level_wag + end + end + + def Dog2.wag + :class_level_wag + end + + def test_since_classes_are_objects_you_can_define_singleton_methods_on_them_too + assert_equal __(:class_level_wag), Dog2.wag + end + + def test_class_methods_are_independent_of_instance_methods + fido = Dog2.new + assert_equal __(:instance_level_wag), fido.wag + assert_equal __(:class_level_wag), Dog2.wag + end + + # ------------------------------------------------------------------ + + class Dog + attr_accessor :name + end + + def Dog.name + @name + end + + def test_classes_and_instances_do_not_share_instance_variables + fido = Dog.new + fido.name = "Fido" + assert_equal __("Fido"), fido.name + assert_equal __(nil), Dog.name + end + + # ------------------------------------------------------------------ + + class Dog + def Dog.a_class_method + :dogs_class_method + end + end + + def test_you_can_define_class_methods_inside_the_class + assert_equal __(:dogs_class_method), Dog.a_class_method + end + + + # ------------------------------------------------------------------ + + LastExpressionInClassStatement = class Dog + 21 + end + + def test_class_statements_return_the_value_of_their_last_expression + assert_equal __(21), LastExpressionInClassStatement + end + + # ------------------------------------------------------------------ + + SelfInsideOfClassStatement = class Dog + self + end + + def test_self_while_inside_class_is_class_object_not_instance + assert_equal __(true), Dog == SelfInsideOfClassStatement + end + + # ------------------------------------------------------------------ + + class Dog + def self.class_method2 + :another_way_to_write_class_methods + end + end + + def test_you_can_use_self_instead_of_an_explicit_reference_to_dog + assert_equal __(:another_way_to_write_class_methods), Dog.class_method2 + end + + # ------------------------------------------------------------------ + + class Dog + class << self + def another_class_method + :still_another_way + end + end + end + + def test_heres_still_another_way_to_write_class_methods + assert_equal __(:still_another_way), Dog.another_class_method + end + + # THINK ABOUT IT: + # + # The two major ways to write class methods are: + # class Demo + # def self.method + # end + # + # class << self + # def class_methods + # end + # end + # end + # + # Which do you prefer and why? + # Are there times you might prefer one over the other? + + # ------------------------------------------------------------------ + + def test_heres_an_easy_way_to_call_class_methods_from_instance_methods + fido = Dog.new + assert_equal __(:still_another_way), fido.class.another_class_method + end + +end diff --git a/src/about_classes.rb b/src/about_classes.rb new file mode 100644 index 000000000..b8b67ea12 --- /dev/null +++ b/src/about_classes.rb @@ -0,0 +1,190 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutClasses < EdgeCase::Koan + class Dog + end + + def test_instances_of_classes_can_be_created_with_new + fido = Dog.new + assert_equal __(Dog), fido.class + end + + # ------------------------------------------------------------------ + + class Dog2 + def set_name(a_name) + @name = a_name + end + end + + def test_instance_variables_can_be_set_by_assigning_to_them + fido = Dog2.new + assert_equal __([]), fido.instance_variables + + fido.set_name("Fido") + assert_equal __(["@name"], [:@name]), fido.instance_variables + end + + def test_instance_variables_cannot_be_accessed_outside_the_class + fido = Dog2.new + fido.set_name("Fido") + + assert_raise(___(NoMethodError)) do + fido.name + end + + assert_raise(___(SyntaxError)) do + eval "fido.@name" + # NOTE: Using eval because the above line is a syntax error. + end + end + + def test_you_can_politely_ask_for_instance_variable_values + fido = Dog2.new + fido.set_name("Fido") + + assert_equal __("Fido"), fido.instance_variable_get("@name") + end + + def test_you_can_rip_the_value_out_using_instance_eval + fido = Dog2.new + fido.set_name("Fido") + + assert_equal __("Fido"), fido.instance_eval("@name") # string version + assert_equal __("Fido"), fido.instance_eval { @name } # block version + end + + # ------------------------------------------------------------------ + + class Dog3 + def set_name(a_name) + @name = a_name + end + def name + @name + end + end + + def test_you_can_create_accessor_methods_to_return_instance_variables + fido = Dog3.new + fido.set_name("Fido") + + assert_equal __("Fido"), fido.name + end + + # ------------------------------------------------------------------ + + class Dog4 + attr_reader :name + + def set_name(a_name) + @name = a_name + end + end + + + def test_attr_reader_will_automatically_define_an_accessor + fido = Dog4.new + fido.set_name("Fido") + + assert_equal __("Fido"), fido.name + end + + # ------------------------------------------------------------------ + + class Dog5 + attr_accessor :name + end + + + def test_attr_accessor_will_automatically_define_both_read_and_write_accessors + fido = Dog5.new + + fido.name = "Fido" + assert_equal __("Fido"), fido.name + end + + # ------------------------------------------------------------------ + + class Dog6 + attr_reader :name + def initialize(initial_name) + @name = initial_name + end + end + + def test_initialize_provides_initial_values_for_instance_variables + fido = Dog6.new("Fido") + assert_equal __("Fido"), fido.name + end + + def test_args_to_new_must_match_initialize + assert_raise(___(ArgumentError)) do + Dog6.new + end + # THINK ABOUT IT: + # Why is this so? + end + + def test_different_objects_have_difference_instance_variables + fido = Dog6.new("Fido") + rover = Dog6.new("Rover") + + assert_equal __(true), rover.name != fido.name + end + + # ------------------------------------------------------------------ + + class Dog7 + attr_reader :name + + def initialize(initial_name) + @name = initial_name + end + + def get_self + self + end + + def to_s + __(@name) + end + + def inspect + "" + end + end + + def test_inside_a_method_self_refers_to_the_containing_object + fido = Dog7.new("Fido") + + fidos_self = fido.get_self + assert_equal __(fido), fidos_self + end + + def test_to_s_provides_a_string_version_of_the_object + fido = Dog7.new("Fido") + assert_equal __("Fido"), fido.to_s + end + + def test_to_s_is_used_in_string_interpolation + fido = Dog7.new("Fido") + assert_equal __("My dog is Fido"), "My dog is #{fido}" + end + + def test_inspect_provides_a_more_complete_string_version + fido = Dog7.new("Fido") + assert_equal __(""), fido.inspect + end + + def test_all_objects_support_to_s_and_inspect + array = [1,2,3] + + assert_equal __("123", "[1, 2, 3]"), array.to_s + assert_equal __("[1, 2, 3]"), array.inspect + + assert_equal __("STRING"), "STRING".to_s + assert_equal __('"STRING"'), "STRING".inspect + end + +end diff --git a/src/about_constants.rb b/src/about_constants.rb new file mode 100644 index 000000000..ad780c698 --- /dev/null +++ b/src/about_constants.rb @@ -0,0 +1,87 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +C = "top level" + +class AboutConstants < EdgeCase::Koan + + C = "nested" + + def test_nested_constants_may_also_be_referenced_with_relative_paths + assert_equal __("nested"), C + end + + def test_top_level_constants_are_referenced_by_double_colons + assert_equal __("top level"), ::C + end + + def test_nested_constants_are_referenced_by_their_complete_path + assert_equal __("nested"), AboutConstants::C + assert_equal __("nested"), ::AboutConstants::C + end + + # ------------------------------------------------------------------ + + class Animal + LEGS = 4 + def legs_in_animal + LEGS + end + + class NestedAnimal + def legs_in_nested_animal + LEGS + end + end + end + + def test_nested_classes_inherit_constants_from_enclosing_classes + assert_equal __(4), Animal::NestedAnimal.new.legs_in_nested_animal + end + + # ------------------------------------------------------------------ + + class Reptile < Animal + def legs_in_reptile + LEGS + end + end + + def test_subclasses_inherit_constants_from_parent_classes + assert_equal __(4), Reptile.new.legs_in_reptile + end + + # ------------------------------------------------------------------ + + class MyAnimals + LEGS = 2 + + class Bird < Animal + def legs_in_bird + LEGS + end + end + end + + def test_who_wins_with_both_nested_and_inherited_constants + assert_equal __(2), MyAnimals::Bird.new.legs_in_bird + end + + # QUESTION: Which has precedence: The constant in the lexical scope, + # or the constant from the inheritance heirarachy? + + # ------------------------------------------------------------------ + + class MyAnimals::Oyster < Animal + def legs_in_oyster + LEGS + end + end + + def test_who_wins_with_explicit_scoping_on_class_definition + assert_equal __(4), MyAnimals::Oyster.new.legs_in_oyster + end + + # QUESTION: Now Which has precedence: The constant in the lexical + # scope, or the constant from the inheritance heirarachy? Why is it + # different than the previous answer? +end diff --git a/src/about_control_statements.rb b/src/about_control_statements.rb new file mode 100644 index 000000000..319bde005 --- /dev/null +++ b/src/about_control_statements.rb @@ -0,0 +1,126 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutControlStatements < EdgeCase::Koan + + def test_if_then_else_statements + if true + result = :true_value + else + result = :false_value + end + assert_equal __(:true_value), result + end + + def test_if_then_statements + result = :default_value + if true + result = :true_value + end + assert_equal __(:true_value), result + end + + def test_if_statements_return_values + value = if true + :true_value + else + :false_value + end + assert_equal __(:true_value), value + + value = if false + :true_value + else + :false_value + end + assert_equal __(:false_value), value + + # NOTE: Actually, EVERY statement in Ruby will return a value, not + # just if statements. + end + + def test_if_statements_with_no_else_with_false_condition_return_value + value = if false + :true_value + end + assert_equal __(nil), value + end + + def test_condition_operators + assert_equal __(:true_value), (true ? :true_value : :false_value) + assert_equal __(:false_value), (false ? :true_value : :false_value) + end + + def test_if_statement_modifiers + result = :default_value + result = :true_value if true + + assert_equal __(:true_value), result + end + + def test_unless_statement + result = :default_value + unless false + result = :false_value + end + assert_equal __(:false_value), result + end + + def test_unless_statement_modifier + result = :default_value + result = :false_value unless false + + assert_equal __(:false_value), result + end + + def test_while_statement + i = 1 + result = 1 + while i <= 10 + result = result * i + i += 1 + end + assert_equal __(3628800), result + end + + def test_break_statement + i = 1 + result = 1 + while true + break unless i <= 10 + result = result * i + i += 1 + end + assert_equal __(3628800), result + end + + def test_break_statement_returns_values + i = 1 + result = while i <= 10 + break i if i % 2 == 0 + i += 1 + end + + assert_equal __(2), result + end + + def test_next_statement + i = 0 + result = [] + while i < 10 + i += 1 + next if (i % 2) == 0 + result << i + end + assert_equal __([1, 3, 5, 7, 9]), result + end + + def test_for_statement + array = ["fish", "and", "chips"] + result = [] + for item in array + result << item.upcase + end + assert_equal [__("FISH"), __("AND"), __("CHIPS")], result + end + +end diff --git a/src/about_dice_project.rb b/src/about_dice_project.rb new file mode 100644 index 000000000..65c21dfe7 --- /dev/null +++ b/src/about_dice_project.rb @@ -0,0 +1,72 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +# Implement a DiceSet Class here: +# +# class DiceSet +# code ... +# end + +#-- +class DiceSet + attr_reader :values + def roll(n) + @values = (1..n).map { rand(6) + 1 } + end +end + +#++ +class AboutDiceProject < EdgeCase::Koan + def test_can_create_a_dice_set + dice = DiceSet.new + assert_not_nil dice + end + + def test_rolling_the_dice_returns_a_set_of_integers_between_1_and_6 + dice = DiceSet.new + + dice.roll(5) + assert dice.values.is_a?(Array), "should be an array" + assert_equal 5, dice.values.size + dice.values.each do |value| + assert value >= 1 && value <= 6, "value #{value} must be between 1 and 6" + end + end + + def test_dice_values_do_not_change_unless_explicitly_rolled + dice = DiceSet.new + dice.roll(5) + first_time = dice.values + second_time = dice.values + assert_equal first_time, second_time + end + + def test_dice_values_should_change_between_rolls + dice = DiceSet.new + + dice.roll(5) + first_time = dice.values + + dice.roll(5) + second_time = dice.values + + assert_not_equal first_time, second_time, + "Two rolls should not be equal" + + # THINK ABOUT IT: + # + # If the rolls are random, then it is possible (although not + # likely) that two consecutive rolls are equal. What would be a + # better way to test this. + end + + def test_you_can_roll_different_numbers_of_dice + dice = DiceSet.new + + dice.roll(3) + assert_equal 3, dice.values.size + + dice.roll(1) + assert_equal 1, dice.values.size + end + +end diff --git a/src/about_exceptions.rb b/src/about_exceptions.rb new file mode 100644 index 000000000..f24398249 --- /dev/null +++ b/src/about_exceptions.rb @@ -0,0 +1,68 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutExceptions < EdgeCase::Koan + + class MySpecialError < RuntimeError + end + + def test_exceptions_inherit_from_Exception + assert_equal __(RuntimeError), MySpecialError.ancestors[1] + assert_equal __(StandardError), MySpecialError.ancestors[2] + assert_equal __(Exception), MySpecialError.ancestors[3] + assert_equal __(Object), MySpecialError.ancestors[4] + end + + def test_rescue_clause + result = nil + begin + fail "Oops" + rescue StandardError => ex + result = :exception_handled + end + + assert_equal __(:exception_handled), result + + assert_equal __(true), ex.is_a?(StandardError), "Should be a Standard Error" + assert_equal __(true), ex.is_a?(RuntimeError), "Should be a Runtime Error" + + assert RuntimeError.ancestors.include?(StandardError), # __ + "RuntimeError is a subclass of StandardError" + + assert_equal __("Oops"), ex.message + end + + def test_raising_a_particular_error + result = nil + begin + # 'raise' and 'fail' are synonyms + raise MySpecialError, "My Message" + rescue MySpecialError => ex + result = :exception_handled + end + + assert_equal __(:exception_handled), result + assert_equal __("My Message"), ex.message + end + + def test_ensure_clause + result = nil + begin + fail "Oops" + rescue StandardError => ex + # no code here + ensure + result = :always_run + end + + assert_equal __(:always_run), result + end + + # Sometimes, we must know about the unknown + def test_asserting_an_error_is_raised # __ + # A do-end is a block, a topic to explore more later + assert_raise(___(MySpecialError)) do + raise MySpecialError.new("New instances can be raised directly.") + end + end + +end diff --git a/src/about_extra_credit.rb b/src/about_extra_credit.rb new file mode 100644 index 000000000..5012edf8c --- /dev/null +++ b/src/about_extra_credit.rb @@ -0,0 +1,8 @@ +# EXTRA CREDIT: +# +# Create a program that will play the Greed Game. +# Rules for the game are in GREED_RULES.TXT. +# +# You already have a DiceSet class and score function you can use. +# Write a player class and a Game class to complete the project. This +# is a free form assignment, so approach it however you desire. diff --git a/src/about_hashes.rb b/src/about_hashes.rb new file mode 100644 index 000000000..49b393915 --- /dev/null +++ b/src/about_hashes.rb @@ -0,0 +1,66 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutHashes < EdgeCase::Koan + def test_creating_hashes + empty_hash = Hash.new + assert_equal __(Hash), empty_hash.class + assert_equal({}, empty_hash) # __ + assert_equal __(0), empty_hash.size + end + + def test_hash_literals + hash = { :one => "uno", :two => "dos" } + assert_equal __(2), hash.size + end + + def test_accessing_hashes + hash = { :one => "uno", :two => "dos" } + assert_equal __("uno"), hash[:one] + assert_equal __("dos"), hash[:two] + assert_equal __(nil), hash[:doesnt_exist] + end + + def test_changing_hashes + hash = { :one => "uno", :two => "dos" } + hash[:one] = "eins" + + expected = { :one => __("eins"), :two => "dos" } + assert_equal __(true), expected == hash + + # Bonus Question: Why was "expected" broken out into a variable + # rather than used as a literal? + end + + def test_hash_is_unordered + hash1 = { :one => "uno", :two => "dos" } + hash2 = { :two => "dos", :one => "uno" } + + assert_equal __(true), hash1 == hash2 + end + + def test_hash_keys + hash = { :one => "uno", :two => "dos" } + assert_equal __(2), hash.keys.size + assert_equal __(true), hash.keys.include?(:one) + assert_equal __(true), hash.keys.include?(:two) + assert_equal __(Array), hash.keys.class + end + + def test_hash_values + hash = { :one => "uno", :two => "dos" } + assert_equal __(2), hash.values.size + assert_equal __(true), hash.values.include?("uno") + assert_equal __(true), hash.values.include?("dos") + assert_equal __(Array), hash.values.class + end + + def test_combining_hashes + hash = { "jim" => 53, "amy" => 20, "dan" => 23 } + new_hash = hash.merge({ "jim" => 54, "jenny" => 26 }) + + assert_equal __(true), hash != new_hash + + expected = { "jim" => __(54), "amy" => 20, "dan" => 23, "jenny" => __(26) } + assert_equal __(true), expected == new_hash + end +end diff --git a/src/about_inheritance.rb b/src/about_inheritance.rb new file mode 100644 index 000000000..73030c6ef --- /dev/null +++ b/src/about_inheritance.rb @@ -0,0 +1,85 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutInheritance < EdgeCase::Koan + class Dog + attr_reader :name + + def initialize(name) + @name = name + end + + def bark + "WOOF" + end + end + + class Chihuahua < Dog + def wag + :happy + end + + def bark + "yip" + end + end + + def test_subclasses_have_the_parent_as_an_ancestor + assert_equal __(true), Chihuahua.ancestors.include?(Dog) + end + + def test_all_classes_ultimately_inherit_from_object + assert_equal __(true), Chihuahua.ancestors.include?(Object) + end + + def test_subclasses_inherit_behavior_from_parent_class + chico = Chihuahua.new("Chico") + assert_equal __("Chico"), chico.name + end + + def test_subclasses_add_new_behavior + chico = Chihuahua.new("Chico") + assert_equal __(:happy), chico.wag + + assert_raise(___(NoMethodError)) do + fido = Dog.new("Fido") + fido.wag + end + end + + def test_subclasses_can_modify_existing_behavior + chico = Chihuahua.new("Chico") + assert_equal __("yip"), chico.bark + + fido = Dog.new("Fido") + assert_equal __("WOOF"), fido.bark + end + + # ------------------------------------------------------------------ + + class BullDog < Dog + def bark + super + ", GROWL" + end + end + + def test_subclasses_can_invoke_parent_behavior_via_super + ralph = BullDog.new("Ralph") + assert_equal __("WOOF, GROWL"), ralph.bark + end + + # ------------------------------------------------------------------ + + class GreatDane < Dog + def growl + super.bark + ", GROWL" + end + end + + def test_super_does_not_work_cross_method + george = GreatDane.new("George") + assert_raise(___(NoMethodError)) do + george.growl + end + end + +end diff --git a/src/about_iteration.rb b/src/about_iteration.rb new file mode 100644 index 000000000..324a116f8 --- /dev/null +++ b/src/about_iteration.rb @@ -0,0 +1,103 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutIteration < EdgeCase::Koan + + def test_each_is_a_method_on_arrays + [].methods.include?("each") + end + + def test_iterating_with_each + array = [1, 2, 3] + sum = 0 + array.each do |item| + sum += item + end + assert_equal __(6), sum + end + + def test_each_can_use_curly_brace_blocks_too + array = [1, 2, 3] + sum = 0 + array.each { |item| + sum += item + } + assert_equal __(6), sum + end + + def test_break_works_with_each_style_iterations + array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + sum = 0 + array.each { |item| + break if item > 3 + sum += item + } + assert_equal __(6), sum + end + + def test_collect_transforms_elements_of_an_array + array = [1, 2, 3] + new_array = array.collect { |item| item + 10 } + assert_equal __([11, 12, 13]), new_array + + # NOTE: 'map' is another name for the 'collect' operation + another_array = array.map { |item| item + 10 } + assert_equal __([11, 12, 13]), another_array + end + + def test_select_selects_certain_items_from_an_array + array = [1, 2, 3, 4, 5, 6] + + even_numbers = array.select { |item| (item % 2) == 0 } + assert_equal __([2, 4, 6]), even_numbers + + # NOTE: 'find_all' is another name for the 'select' operation + more_even_numbers = array.find_all { |item| (item % 2) == 0 } + assert_equal __([2, 4, 6]), more_even_numbers + end + + def test_find_locates_the_first_element_matching_a_criteria + array = ["Jim", "Bill", "Clarence", "Doug", "Eli"] + + assert_equal __("Clarence"), array.find { |item| item.size > 4 } + end + + def test_inject_will_blow_your_mind + result = [2, 3, 4].inject(0) { |sum, item| sum + item } + assert_equal __(9), result + + result2 = [2, 3, 4].inject(1) { |sum, item| sum * item } + assert_equal __(24), result2 + + # Extra Credit: + # Describe in your own words what inject does. + end + + def test_all_iteration_methods_work_on_any_collection_not_just_arrays + # Ranges act like a collection + result = (1..3).map { |item| item + 10 } + assert_equal __([11, 12, 13]), result + + # Files act like a collection of lines + File.open("example_file.txt") do |file| + upcase_lines = file.map { |line| line.strip.upcase } + assert_equal __(["THIS", "IS", "A", "TEST"]), upcase_lines + end + + # NOTE: You can create your own collections that work with each, + # map, select, etc. + end + + # Bonus Question: In the previous koan, we saw the construct: + # + # File.open(filename) do |file| + # # code to read 'file' + # end + # + # Why did we do it that way instead of the following? + # + # file = File.open(filename) + # # code to read 'file' + # + # When you get to the "AboutSandwichCode" koan, recheck your answer. + +end diff --git a/src/about_java_interop.rb b/src/about_java_interop.rb new file mode 100644 index 000000000..c2d2142ce --- /dev/null +++ b/src/about_java_interop.rb @@ -0,0 +1,137 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +include Java + +# Concepts +# * Pull in a java class +# * calling a method, Camel vs snake +# * Resolving module/class name conflicts +# * Showing what gets returned +# * Ruby Strings VS Java Strings +# * Calling custom java class +# * Calling Ruby from java??? + +class AboutJavaInterop < EdgeCase::Koan + def test_using_a_java_library_class + java_array = java.util.ArrayList.new + assert_equal __(Java::JavaUtil::ArrayList), java_array.class + end + + def test_java_class_can_be_referenced_using_both_ruby_and_java_like_syntax + assert_equal __(true), Java::JavaUtil::ArrayList == java.util.ArrayList + end + + def test_include_class_includes_class_in_module_scope + assert_nil defined?(TreeSet) # __ + include_class "java.util.TreeSet" + assert_equal __("constant"), defined?(TreeSet) + end + + # THINK ABOUT IT: + # + # What if we use: + # + # include_class "java.lang.String" + # + # What would be the value of the String constant after this + # include_class is run? Would it be useful to provide a way of + # aliasing java classes to different names? + + JString = java.lang.String + def test_also_java_class_can_be_given_ruby_aliases + java_string = JString.new("A Java String") + assert_equal __(java.lang.String), java_string.class + assert_equal __(java.lang.String), JString + end + + def test_can_directly_call_java_methods_on_java_objects + java_string = JString.new("A Java String") + assert_equal __("a java string"), java_string.toLowerCase + end + + def test_jruby_provides_snake_case_versions_of_java_methods + java_string = JString.new("A Java String") + assert_equal __("a java string"), java_string.to_lower_case + end + + def test_jruby_provides_question_mark_versions_of_boolean_methods + java_string = JString.new("A Java String") + assert_equal __(true), java_string.endsWith("String") + assert_equal __(true), java_string.ends_with("String") + assert_equal __(true), java_string.ends_with?("String") + end + + def test_java_string_are_not_ruby_strings + ruby_string = "A Java String" + java_string = java.lang.String.new(ruby_string) + assert_equal __(true), java_string.is_a?(java.lang.String) + assert_equal __(false), java_string.is_a?(String) + end + + def test_java_strings_can_be_compared_to_ruby_strings_maybe + ruby_string = "A Java String" + java_string = java.lang.String.new(ruby_string) + assert_equal __(false), ruby_string == java_string + assert_equal __(true), java_string == ruby_string + + # THINK ABOUT IT: + # + # Is there any possible way for this to be more wrong? + # + # SERIOUSLY, THINK ABOUT IT: + # + # Why do you suppose that Ruby and Java strings compare like that? + # + # ADVANCED THINK ABOUT IT: + # + # Is there a way to make Ruby/Java string comparisons commutative? + # How would you do it? + end + + def test_however_most_methods_returning_strings_return_ruby_strings + java_array = java.util.ArrayList.new + assert_equal __("[]"), java_array.toString + assert_equal __(true), java_array.toString.is_a?(String) + assert_equal __(false), java_array.toString.is_a?(java.lang.String) + end + + def test_some_ruby_objects_can_be_coerced_to_java + assert_equal __(Java::JavaLang::String), "ruby string".to_java.class + assert_equal __(Java::JavaLang::Long), 1.to_java.class + assert_equal __(Java::JavaLang::Double), 9.32.to_java.class + assert_equal __(Java::JavaLang::Boolean), false.to_java.class + end + + def test_some_ruby_objects_are_not_coerced_to_what_you_might_expect + assert_equal __(false), [].to_java.class == Java::JavaUtil::ArrayList + assert_equal __(false), {}.to_java.class == Java::JavaUtil::HashMap + assert_equal __(false), Object.new.to_java.class == Java::JavaLang::Object + end + + def test_java_collections_are_enumerable + java_array = java.util.ArrayList.new + java_array << "one" << "two" << "three" + assert_equal __(["ONE", "TWO", "THREE"]), java_array.map { |item| item.upcase } + end + + # ------------------------------------------------------------------ + + # Open the Java ArrayList class and add a new method. + class Java::JavaUtil::ArrayList + def multiply_all + result = 1 + each do |item| + result *= item + end + result + end + end + + def test_java_class_are_open_from_ruby + java_array = java.util.ArrayList.new + java_array.add_all([1,2,3,4,5]) + + assert_equal __(120), java_array.multiply_all + end + +end diff --git a/src/about_message_passing.rb b/src/about_message_passing.rb new file mode 100644 index 000000000..882716ecc --- /dev/null +++ b/src/about_message_passing.rb @@ -0,0 +1,178 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutMessagePassing < EdgeCase::Koan + + class MessageCatcher + def caught? + true + end + end + + def test_methods_can_be_called_directly + mc = MessageCatcher.new + + assert mc.caught? # __ + end + + def test_methods_can_be_invoked_by_sending_the_message + mc = MessageCatcher.new + + assert mc.send(:caught?) # __ + end + + def test_methods_can_be_invoked_more_dynamically + mc = MessageCatcher.new + + assert mc.send("caught?") # __ + assert mc.send("caught" + __("?") ) # What do you need to add to the first string? + assert mc.send("CAUGHT?".____(:downcase) ) # What would you need to do to the string? + end + + def test_send_with_underscores_will_also_send_messages + mc = MessageCatcher.new + + assert_equal __(true), mc.__send__(:caught?) + + # THINK ABOUT IT: + # + # Why does Ruby provide both send and __send__ ? + end + + def test_classes_can_be_asked_if_they_know_how_to_respond + mc = MessageCatcher.new + + assert_equal __(true), mc.respond_to?(:caught?) + assert_equal __(false), mc.respond_to?(:does_not_exist) + end + + # ------------------------------------------------------------------ + + class MessageCatcher + def add_a_payload(*args) + args + end + end + + def test_sending_a_message_with_arguments + mc = MessageCatcher.new + + assert_equal __([]), mc.add_a_payload + assert_equal __([]), mc.send(:add_a_payload) + + assert_equal __([3, 4, nil, 6]), mc.add_a_payload(3, 4, nil, 6) + assert_equal __([3, 4, nil, 6]), mc.send(:add_a_payload, 3, 4, nil, 6) + end + + # ------------------------------------------------------------------ + + class TypicalObject + end + + def test_sending_undefined_messages_to_a_typical_object_results_in_errors + typical = TypicalObject.new + + exception = assert_raise(___(NoMethodError)) do + typical.foobar + end + assert_match(/foobar/, exception.message) # __ + end + + def test_calling_method_missing_causes_the_no_method_error + typical = TypicalObject.new + + exception = assert_raise(___(NoMethodError)) do + typical.method_missing(:foobar) + end + assert_match(/foobar/, exception.message) # __ + + # THINK ABOUT IT: + # + # If the method :method_missing causes the NoMethodError, then + # what would happen if we redefine method_missing? + # + # NOTE: + # + # In Ruby 1.8 the method_missing method is public and can be + # called as shown above. However, in Ruby 1.9 the method_missing + # method is private. We explicitly made it public in the testing + # framework so this example works in both versions of Ruby. Just + # keep in mind you can't call method_missing like that in Ruby + # 1.9. normally. + # + # Thanks. We now return you to your regularly scheduled Ruby + # Koans. + end + + # ------------------------------------------------------------------ + + class AllMessageCatcher + def method_missing(method_name, *args, &block) + "Someone called #{method_name} with <#{args.join(", ")}>" + end + end + + def test_all_messages_are_caught + catcher = AllMessageCatcher.new + + assert_equal __("Someone called foobar with <>"), catcher.foobar + assert_equal __("Someone called foobaz with <1>"), catcher.foobaz(1) + assert_equal __("Someone called sum with <1, 2, 3, 4, 5, 6>"), catcher.sum(1,2,3,4,5,6) + end + + def test_catching_messages_makes_respond_to_lie + catcher = AllMessageCatcher.new + + assert_nothing_raised(NoMethodError) do # __ + catcher.any_method + end + assert_equal __(false), catcher.respond_to?(:any_method) + end + + # ------------------------------------------------------------------ + + class WellBehavedFooCatcher + def method_missing(method_name, *args, &block) + if method_name.to_s[0,3] == "foo" + "Foo to you too" + else + super(method_name, *args, &block) + end + end + end + + def test_foo_method_are_caught + catcher = WellBehavedFooCatcher.new + + assert_equal __("Foo to you too"), catcher.foo_bar + assert_equal __("Foo to you too"), catcher.foo_baz + end + + def test_non_foo_messages_are_treated_normally + catcher = WellBehavedFooCatcher.new + + assert_raise(___(NoMethodError)) do + catcher.normal_undefined_method + end + end + + # ------------------------------------------------------------------ + + # (note: just reopening class from above) + class WellBehavedFooCatcher + def respond_to?(method_name) + if method_name.to_s[0,3] == "foo" + true + else + super(method_name) + end + end + end + + def test_explicitly_implementing_respond_to_lets_objects_tell_the_truth + catcher = WellBehavedFooCatcher.new + + assert_equal __(true), catcher.respond_to?(:foo_bar) + assert_equal __(false), catcher.respond_to?(:something_else) + end + +end diff --git a/src/about_methods.rb b/src/about_methods.rb new file mode 100644 index 000000000..9e7af3744 --- /dev/null +++ b/src/about_methods.rb @@ -0,0 +1,160 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +def my_global_method(a,b) + a + b +end + +class AboutMethods < EdgeCase::Koan + + def test_calling_global_methods + assert_equal __(5), my_global_method(2,3) + end + + def test_calling_global_methods_without_parentheses + result = my_global_method 2, 3 + assert_equal __(5), result + end + + # (NOTE: We are Using eval below because the example code is + # considered to be syntactically invalid). + def test_sometimes_missing_parentheses_are_ambiguous + #-- + eval "assert_equal 5, my_global_method(2, 3)" # REMOVE CHECK # __ + if false + #++ + eval "assert_equal 5, my_global_method 2, 3" # ENABLE CHECK # __ + #-- + end + #++ + # + # Ruby doesn't know if you mean: + # + # assert_equal(5, my_global_method(2), 3) + # or + # assert_equal(5, my_global_method(2, 3)) + # + # Rewrite the eval string to continue. + # + end + + # NOTE: wrong number of argument is not a SYNTAX error, but a + # runtime error. + def test_calling_global_methods_with_wrong_number_of_arguments + exception = assert_raise(___(ArgumentError)) do + my_global_method + end + #-- + pattern = "wrong (number|#) of arguments" + #++ + assert_match(/#{__(pattern)}/, exception.message) + + exception = assert_raise(___(ArgumentError)) do + my_global_method(1,2,3) + end + assert_match(/#{__(pattern)}/, exception.message) + end + + # ------------------------------------------------------------------ + + def method_with_defaults(a, b=:default_value) + [a, b] + end + + def test_calling_with_default_values + assert_equal [1, __(:default_value)], method_with_defaults(1) + assert_equal [1, __(2)], method_with_defaults(1, 2) + end + + # ------------------------------------------------------------------ + + def method_with_var_args(*args) + args + end + + def test_calling_with_variable_arguments + assert_equal __([]), method_with_var_args + assert_equal __([:one]), method_with_var_args(:one) + assert_equal __([:one, :two]), method_with_var_args(:one, :two) + end + + # ------------------------------------------------------------------ + + def method_with_explicit_return + :a_non_return_value + return :return_value + :another_non_return_value + end + + def test_method_with_explicit_return + assert_equal __(:return_value), method_with_explicit_return + end + + # ------------------------------------------------------------------ + + def method_without_explicit_return + :a_non_return_value + :return_value + end + + def test_method_without_explicit_return + assert_equal __(:return_value), method_without_explicit_return + end + + # ------------------------------------------------------------------ + + def my_method_in_the_same_class(a, b) + a * b + end + + def test_calling_methods_in_same_class + assert_equal __(12), my_method_in_the_same_class(3,4) + end + + def test_calling_methods_in_same_class_with_explicit_receiver + assert_equal __(12), self.my_method_in_the_same_class(3,4) + end + + # ------------------------------------------------------------------ + + def my_private_method + "a secret" + end + private :my_private_method + + def test_calling_private_methods_without_receiver + assert_equal __("a secret"), my_private_method + end + + def test_calling_private_methods_with_an_explicit_receiver + exception = assert_raise(___(NoMethodError)) do + self.my_private_method + end + assert_match /#{__("private method `my_private_method' called ")}/, exception.message + end + + # ------------------------------------------------------------------ + + class Dog + def name + "Fido" + end + + private + + def tail + "tail" + end + end + + def test_calling_methods_in_other_objects_require_explicit_receiver + rover = Dog.new + assert_equal __("Fido"), rover.name + end + + def test_calling_private_methods_in_other_objects + rover = Dog.new + assert_raise(___(NoMethodError)) do + rover.tail + end + end +end diff --git a/src/about_modules.rb b/src/about_modules.rb new file mode 100644 index 000000000..261746a58 --- /dev/null +++ b/src/about_modules.rb @@ -0,0 +1,63 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutModules < EdgeCase::Koan + module Nameable + def set_name(new_name) + @name = new_name + end + + def here + :in_module + end + end + + def test_cant_instantiate_modules + assert_raise(___(NoMethodError)) do + Nameable.new + end + end + + # ------------------------------------------------------------------ + + class Dog + include Nameable + + attr_reader :name + + def initialize + @name = "Fido" + end + + def bark + "WOOF" + end + + def here + :in_object + end + end + + def test_normal_methods_are_available_in_the_object + fido = Dog.new + assert_equal __("WOOF"), fido.bark + end + + def test_module_methods_are_also_availble_in_the_object + fido = Dog.new + assert_nothing_raised(Exception) do # __ + fido.set_name("Rover") + end + end + + def test_module_methods_can_affect_instance_variables_in_the_object + fido = Dog.new + assert_equal __("Fido"), fido.name + fido.set_name("Rover") + assert_equal __("Rover"), fido.name + end + + def test_classes_can_override_module_methods + fido = Dog.new + assert_equal __(:in_object), fido.here + end +end diff --git a/src/about_nil.rb b/src/about_nil.rb new file mode 100644 index 000000000..1e670a079 --- /dev/null +++ b/src/about_nil.rb @@ -0,0 +1,38 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutNil < EdgeCase::Koan + def test_nil_is_an_object + assert_equal __(true), nil.is_a?(Object), "Unlike NULL in other languages" + end + + def test_you_dont_get_null_pointer_errors_when_calling_methods_on_nil + # What happens when you call a method that doesn't exist. The + # following begin/rescue/end code block captures the exception and + # make some assertions about it. + begin + nil.some_method_nil_doesnt_know_about + rescue Exception => ex + # What exception has been caught? + assert_equal __(NoMethodError), ex.class + + # What message was attached to the exception? + # (HINT: replace __ with part of the error message.) + assert_match(/#{__("undefined method")}/, ex.message) + end + end + + def test_nil_has_a_few_methods_defined_on_it + assert_equal __(true), nil.nil? + assert_equal __(""), nil.to_s + assert_equal __("nil"), nil.inspect + + # THINK ABOUT IT: + # + # Is it better to use + # obj.nil? + # or + # obj == nil + # Why? + end + +end diff --git a/src/about_objects.rb b/src/about_objects.rb new file mode 100644 index 000000000..1faf0a0eb --- /dev/null +++ b/src/about_objects.rb @@ -0,0 +1,56 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutObjects < EdgeCase::Koan + def test_everything_is_an_object + assert_equal __(true), 1.is_a?(Object) + assert_equal __(true), 1.5.is_a?(Object) + assert_equal __(true), "string".is_a?(Object) + assert_equal __(true), nil.is_a?(Object) + assert_equal __(true), Object.is_a?(Object) + end + + def test_objects_can_be_converted_to_strings + assert_equal __("123"), 123.to_s + assert_equal __(""), nil.to_s + end + + def test_objects_can_be_inspected + assert_equal __("123"), 123.inspect + assert_equal __("nil"), nil.inspect + end + + def test_every_object_has_an_id + obj = Object.new + assert_equal __(Fixnum), obj.object_id.class + end + + def test_every_object_has_different_id + obj = Object.new + another_obj = Object.new + assert_equal __(true), obj.object_id != another_obj.object_id + end + + def test_some_system_objects_always_have_the_same_id + assert_equal __(0), false.object_id + assert_equal __(2), true.object_id + assert_equal __(4), nil.object_id + end + + def test_small_integers_have_fixed_ids + assert_equal __(1), 0.object_id + assert_equal __(3), 1.object_id + assert_equal __(5), 2.object_id + assert_equal __(201), 100.object_id + + # THINK ABOUT IT: + # What pattern do the object IDs for small integers follow? + end + + def test_clone_creates_a_different_object + obj = Object.new + copy = obj.clone + + assert_equal __(true), obj != copy + assert_equal __(true), obj.object_id != copy.object_id + end +end diff --git a/src/about_open_classes.rb b/src/about_open_classes.rb new file mode 100644 index 000000000..bef7effde --- /dev/null +++ b/src/about_open_classes.rb @@ -0,0 +1,45 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutOpenClasses < EdgeCase::Koan + class Dog + def bark + "WOOF" + end + end + + def test_as_defined_dogs_do_bark + fido = Dog.new + assert_equal __("WOOF"), fido.bark + end + + # ------------------------------------------------------------------ + + # Open the existing Dog class and add a new method. + class Dog + def wag + "HAPPY" + end + end + + def test_after_reopening_dogs_can_both_wag_and_bark + fido = Dog.new + assert_equal __("HAPPY"), fido.wag + assert_equal __("WOOF"), fido.bark + end + + # ------------------------------------------------------------------ + + class ::Integer + def even? + (self % 2) == 0 + end + end + + def test_even_existing_built_in_classes_can_be_reopened + assert_equal __(false), 1.even? + assert_equal __(true), 2.even? + end + + # NOTE: To understand why we need the :: before Integer, you need to + # become enlightened about scope. +end diff --git a/src/about_proxy_object_project.rb b/src/about_proxy_object_project.rb new file mode 100644 index 000000000..483a7d7c0 --- /dev/null +++ b/src/about_proxy_object_project.rb @@ -0,0 +1,173 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +# Project: Create a Proxy Class +# +# In this assignment, create a proxy class (one is started for you +# below). You should be able to initialize the proxy object with any +# object. Any messages sent to the proxy object should be forwarded +# to the target object. As each message is sent, the proxy should +# record the name of the method sent. +# +# The proxy class is started for you. You will need to add a method +# missing handler and any other supporting methods. The specification +# of the Proxy class is given in the AboutProxyObjectProject koan. + +class Proxy + def initialize(target_object) + @object = target_object + # ADD MORE CODE HERE + #-- + @messages = [] + #++ + end + + # WRITE CODE HERE + #-- + attr_reader :messages + + def method_missing(sym, *args, &block) + @messages << sym + @object.send(sym, *args, &block) + end + + def called?(method) + @messages.include?(method) + end + + def number_of_times_called(method) + @messages.select { |m| m == method }.size + end + #++ +end + +# The proxy object should pass the following Koan: +# +class AboutProxyObjectProject < EdgeCase::Koan + def test_proxy_method_returns_wrapped_object + # NOTE: The Television class is defined below + tv = Proxy.new(Television.new) + + assert tv.instance_of?(Proxy) + end + + def test_tv_methods_still_perform_their_function + tv = Proxy.new(Television.new) + + tv.channel = 10 + tv.power + + assert_equal 10, tv.channel + assert tv.on? + end + + def test_proxy_records_messages_sent_to_tv + tv = Proxy.new(Television.new) + + tv.power + tv.channel = 10 + + assert_equal [:power, :channel=], tv.messages + end + + def test_proxy_handles_invalid_messages + tv = Proxy.new(Television.new) + + assert_raise(NoMethodError) do + tv.no_such_method + end + end + + def test_proxy_reports_methods_have_been_called + tv = Proxy.new(Television.new) + + tv.power + tv.power + + assert tv.called?(:power) + assert ! tv.called?(:channel) + end + + def test_proxy_counts_method_calls + tv = Proxy.new(Television.new) + + tv.power + tv.channel = 48 + tv.power + + assert_equal 2, tv.number_of_times_called(:power) + assert_equal 1, tv.number_of_times_called(:channel=) + assert_equal 0, tv.number_of_times_called(:on?) + end + + def test_proxy_can_record_more_than_just_tv_objects + proxy = Proxy.new("Code Mash 2009") + + proxy.upcase! + result = proxy.split + + assert_equal ["CODE", "MASH", "2009"], result + assert_equal [:upcase!, :split], proxy.messages + end +end + + +# ==================================================================== +# The following code is to support the testing of the Proxy class. No +# changes should be necessary to anything below this comment. + +# Example class using in the proxy testing above. +class Television + attr_accessor :channel + + def power + if @power == :on + @power = :off + else + @power = :on + end + end + + def on? + @power == :on + end +end + +# Tests for the Television class. All of theses tests should pass. +class TelevisionTest < EdgeCase::Koan + def test_it_turns_on + tv = Television.new + + tv.power + assert tv.on? + end + + def test_it_also_turns_off + tv = Television.new + + tv.power + tv.power + + assert ! tv.on? + end + + def test_edge_case_on_off + tv = Television.new + + tv.power + tv.power + tv.power + + assert tv.on? + + tv.power + + assert ! tv.on? + end + + def test_can_set_the_channel + tv = Television.new + + tv.channel = 11 + assert_equal 11, tv.channel + end +end diff --git a/src/about_regular_expressions.rb b/src/about_regular_expressions.rb new file mode 100644 index 000000000..55b85f678 --- /dev/null +++ b/src/about_regular_expressions.rb @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutRegularExpressions < EdgeCase::Koan + def test_a_pattern_is_a_regular_expression + assert_equal __(Regexp), /pattern/.class + end + + def test_a_regexp_can_search_a_string_for_matching_content + assert_equal __("match"), "some matching content"[/match/] + end + + def test_a_failed_match_returns_nil + assert_equal __(nil), "some matching content"[/missing/] + end + + # ------------------------------------------------------------------ + + def test_question_mark_means_optional + assert_equal __("ab"), "abbcccddddeeeee"[/ab?/] + assert_equal __("a"), "abbcccddddeeeee"[/az?/] + end + + def test_plus_means_one_or_more + assert_equal __("bccc"), "abbcccddddeeeee"[/bc+/] + end + + def test_asterisk_means_zero_or_more + assert_equal __("abb"), "abbcccddddeeeee"[/ab*/] + assert_equal __("a"), "abbcccddddeeeee"[/az*/] + assert_equal __(""), "abbcccddddeeeee"[/z*/] + + # THINK ABOUT IT: + # + # When would * fail to match? + end + + # THINK ABOUT IT: + # + # We say that the repetition operators above are "greedy." + # + # Why? + + # ------------------------------------------------------------------ + + def test_the_left_most_match_wins + assert_equal __("a"), "abbccc az"[/az*/] + end + + # ------------------------------------------------------------------ + + def test_character_classes_give_options_for_a_character + animals = ["cat", "bat", "rat", "zat"] + assert_equal __(["cat", "bat", "rat"]), animals.select { |a| a[/[cbr]at/] } + end + + def test_slash_d_is_a_shortcut_for_a_digit_character_class + assert_equal __("42"), "the number is 42"[/[0123456789]+/] + assert_equal __("42"), "the number is 42"[/\d+/] + end + + def test_character_classes_can_include_ranges + assert_equal __("42"), "the number is 42"[/[0-9]+/] + end + + def test_slash_s_is_a_shortcut_for_a_whitespace_character_class + assert_equal __(" \t\n"), "space: \t\n"[/\s+/] + end + + def test_slash_w_is_a_shortcut_for_a_word_character_class + # NOTE: This is more like how a programmer might define a word. + assert_equal __("variable_1"), "variable_1 = 42"[/[a-zA-Z0-9_]+/] + assert_equal __("variable_1"), "variable_1 = 42"[/\w+/] + end + + def test_period_is_a_shortcut_for_any_non_newline_character + assert_equal __("abc"), "abc\n123"[/a.+/] + end + + def test_a_character_class_can_be_negated + assert_equal __("the number is "), "the number is 42"[/[^0-9]+/] + end + + def test_shortcut_character_classes_are_negated_with_capitals + assert_equal __("the number is "), "the number is 42"[/\D+/] + assert_equal __("space:"), "space: \t\n"[/\S+/] + assert_equal __(" = "), "variable_1 = 42"[/\W+/] + end + + # ------------------------------------------------------------------ + + def test_slash_a_anchors_to_the_start_of_the_string + assert_equal __("start"), "start end"[/\Astart/] + assert_equal __(nil), "start end"[/\Aend/] + end + + def test_slash_z_anchors_to_the_end_of_the_string + assert_equal __("end"), "start end"[/end\z/] + assert_equal __(nil), "start end"[/start\z/] + end + + def test_caret_anchors_to_the_start_of_lines + assert_equal __("2"), "num 42\n2 lines"[/^\d+/] + end + + def test_dollar_sign_anchors_to_the_end_of_lines + assert_equal __("42"), "2 lines\nnum 42"[/\d+$/] + end + + def test_slash_b_anchors_to_a_word_boundary + assert_equal __("vines"), "bovine vines"[/\bvine./] + end + + # ------------------------------------------------------------------ + + def test_parentheses_group_contents + assert_equal __("hahaha"), "ahahaha"[/(ha)+/] + end + + # ------------------------------------------------------------------ + + def test_parentheses_also_capture_matched_content_by_number + assert_equal __("Gray"), "Gray, James"[/(\w+), (\w+)/, 1] + assert_equal __("James"), "Gray, James"[/(\w+), (\w+)/, 2] + end + + def test_variables_can_also_be_used_to_access_captures + assert_equal __("Gray, James"), "Name: Gray, James"[/(\w+), (\w+)/] + assert_equal __("Gray"), $1 + assert_equal __("James"), $2 + end + + # ------------------------------------------------------------------ + + def test_a_vertical_pipe_means_or + grays = /(James|Dana|Summer) Gray/ + assert_equal __("James Gray"), "James Gray"[grays] + assert_equal __("Summer"), "Summer Gray"[grays, 1] + assert_equal __(nil), "Jim Gray"[grays, 1] + end + + # THINK ABOUT IT: + # + # Explain the difference between a character class ([...]) and alternation (|). + + # ------------------------------------------------------------------ + + def test_scan_is_like_find_all + assert_equal __(["one", "two", "three"]), "one two-three".scan(/\w+/) + end + + def test_sub_is_like_find_and_replace + assert_equal __("one t-three"), "one two-three".sub(/(t\w*)/) { $1[0, 1] } + end + + def test_gsub_is_like_find_and_replace_all + assert_equal __("one t-t"), "one two-three".gsub(/(t\w*)/) { $1[0, 1] } + end +end diff --git a/src/about_sandwich_code.rb b/src/about_sandwich_code.rb new file mode 100644 index 000000000..07be3fcc9 --- /dev/null +++ b/src/about_sandwich_code.rb @@ -0,0 +1,113 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutSandwichCode < EdgeCase::Koan + + def count_lines(file_name) + file = open(file_name) + count = 0 + while line = file.gets + count += 1 + end + count + ensure + file.close if file + end + + def test_counting_lines + assert_equal __(4), count_lines("example_file.txt") + end + + # ------------------------------------------------------------------ + + def find_line(file_name) + file = open(file_name) + while line = file.gets + return line if line.match(/e/) + end + ensure + file.close if file + end + + def test_finding_lines + assert_equal __("test\n"), find_line("example_file.txt") + end + + # ------------------------------------------------------------------ + # THINK ABOUT IT: + # + # The count_lines and find_line are similar, and yet different. + # They both follow the pattern of "sandwich code". + # + # Sandwich code is code that comes in three parts: (1) the top slice + # of bread, (2) the meat, and (3) the bottom slice of bread. The + # the bread part of the sandwich almost always goes together, but + # the meat part changes all the time. + # + # Because the changing part of the sandwich code is in the middle, + # abstracting the top and bottom bread slices to a library can be + # difficult in many languages. + # + # (Aside for C++ programmers: The idiom of capturing allocated + # pointers in a smart pointer constructor is an attempt to deal with + # the problem of sandwich code for resource allocation.) + # + # Consider the following code: + # + + def file_sandwich(file_name) + file = open(file_name) + yield(file) + ensure + file.close if file + end + + # Now we write: + + def count_lines2(file_name) + file_sandwich(file_name) do |file| + count = 0 + while line = file.gets + count += 1 + end + count + end + end + + def test_counting_lines2 + assert_equal __(4), count_lines2("example_file.txt") + end + + # ------------------------------------------------------------------ + + def find_line2(file_name) + # Rewrite find_line using the file_sandwich library function. + #-- + file_sandwich(file_name) do |file| + file.each do |line| + return line if line =~ /e/ + end + end + #++ + end + + def test_finding_lines2 + assert_equal __("test\n"), find_line2("example_file.txt") + end + + # ------------------------------------------------------------------ + + def count_lines3(file_name) + open(file_name) do |file| + count = 0 + while line = file.gets + count += 1 + end + count + end + end + + def test_open_handles_the_file_sandwich_when_given_a_block + assert_equal __(4), count_lines3("example_file.txt") + end + +end diff --git a/src/about_scope.rb b/src/about_scope.rb new file mode 100644 index 000000000..f6843c870 --- /dev/null +++ b/src/about_scope.rb @@ -0,0 +1,79 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutScope < EdgeCase::Koan + module Jims + class Dog + def identify + :jims_dog + end + end + end + + module Joes + class Dog + def identify + :joes_dog + end + end + end + + def test_dog_is_not_available_in_the_current_scope + assert_raise(___(NameError)) do + fido = Dog.new + end + end + + def test_you_can_reference_nested_classes_using_the_scope_operator + fido = Jims::Dog.new + rover = Joes::Dog.new + assert_equal __(:jims_dog), fido.identify + assert_equal __(:joes_dog), rover.identify + + assert_equal __(true), fido.class != rover.class + assert_equal __(true), Jims::Dog != Joes::Dog + end + + # ------------------------------------------------------------------ + + class String + end + + def test_bare_bones_class_names_assume_the_current_scope + assert_equal __(true), AboutScope::String == String + end + + def test_nested_string_is_not_the_same_as_the_system_string + assert_equal __(false), String == "HI".class + end + + def test_use_the_prefix_scope_operator_to_force_the_global_scope + assert_equal __(true), ::String == "HI".class + end + + # ------------------------------------------------------------------ + + PI = 3.1416 + + def test_constants_are_defined_with_an_initial_uppercase_letter + assert_equal __(3.1416), PI + end + + # ------------------------------------------------------------------ + + MyString = ::String + + def test_class_names_are_just_constants + assert_equal __(true), MyString == ::String + assert_equal __(true), MyString == "HI".class + end + + def test_constants_can_be_looked_up_explicitly + assert_equal __(true), PI == AboutScope.const_get("PI") + assert_equal __(true), MyString == AboutScope.const_get("MyString") + end + + def test_you_can_get_a_list_of_constants_for_any_class_or_module + assert_equal __(["Dog"], [:Dog]), Jims.constants + assert Object.constants.size > _n_(10) + end +end diff --git a/src/about_scoring_project.rb b/src/about_scoring_project.rb new file mode 100644 index 000000000..60b468226 --- /dev/null +++ b/src/about_scoring_project.rb @@ -0,0 +1,95 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +# Greed is a dice game where you roll up to five dice to accumulate +# points. The following "score" function will be used to calculate the +# score of a single roll of the dice. +# +# A greed roll is scored as follows: +# +# * A set of three ones is 1000 points +# +# * A set of three numbers (other than ones) is worth 100 times the +# number. (e.g. three fives is 500 points). +# +# * A one (that is not part of a set of three) is worth 100 points. +# +# * A five (that is not part of a set of three) is worth 50 points. +# +# * Everything else is worth 0 points. +# +# +# Examples: +# +# score([1,1,1,5,1]) => 1150 points +# score([2,3,4,6,2]) => 0 points +# score([3,4,5,3,3]) => 350 points +# score([1,5,1,2,4]) => 250 points +# +# More scoring examples are given in the tests below: +# +# Your goal is to write the score method. + +def score(dice) + # You need to write this method + #-- + result = 0 + (1..6).each do |face| + count = dice.select { |n| n == face }.size + while count > 0 + if count >= 3 + result += (face == 1) ? 1000 : 100 * face + count -= 3 + elsif face == 5 + result += count * 50 + count = 0 + elsif face == 1 + result += count * 100 + count = 0 + else + count = 0 + end + end + end + result + #++ +end + +class AboutScoringProject < EdgeCase::Koan + def test_score_of_an_empty_list_is_zero + assert_equal 0, score([]) + end + + def test_score_of_a_single_roll_of_5_is_50 + assert_equal 50, score([5]) + end + + def test_score_of_a_single_roll_of_1_is_100 + assert_equal 100, score([1]) + end + + def test_score_of_multiple_1s_and_5s_is_the_sum_of_individual_scores + assert_equal 300, score([1,5,5,1]) + end + + def test_score_of_single_2s_3s_4s_and_6s_are_zero + assert_equal 0, score([2,3,4,6]) + end + + def test_score_of_a_triple_1_is_1000 + assert_equal 1000, score([1,1,1]) + end + + def test_score_of_other_triples_is_100x + assert_equal 200, score([2,2,2]) + assert_equal 300, score([3,3,3]) + assert_equal 400, score([4,4,4]) + assert_equal 500, score([5,5,5]) + assert_equal 600, score([6,6,6]) + end + + def test_score_of_mixed_is_sum + assert_equal 250, score([2,5,2,2,3]) + assert_equal 550, score([5,5,5,5]) + end + +end diff --git a/src/about_strings.rb b/src/about_strings.rb new file mode 100644 index 000000000..dd13fecfd --- /dev/null +++ b/src/about_strings.rb @@ -0,0 +1,195 @@ +require File.expand_path(File.dirname(__FILE__) + '/edgecase') + +class AboutStrings < EdgeCase::Koan + def test_double_quoted_strings_are_strings + string = "Hello, World" + assert_equal __(true), string.is_a?(String) + end + + def test_single_quoted_strings_are_also_strings + string = 'Goodbye, World' + assert_equal __(true), string.is_a?(String) + end + + def test_use_single_quotes_to_create_string_with_double_quotes + string = 'He said, "Go Away."' + assert_equal __('He said, "Go Away."'), string + end + + def test_use_double_quotes_to_create_strings_with_single_quotes + string = "Don't" + assert_equal __("Don't"), string + end + + def test_use_backslash_for_those_hard_cases + a = "He said, \"Don't\"" + b = 'He said, "Don\'t"' + assert_equal __(true), a == b + end + + def test_use_flexible_quoting_to_handle_really_hard_cases + a = %(flexible quotes can handle both ' and " characters) + b = %!flexible quotes can handle both ' and " characters! + c = %{flexible quotes can handle both ' and " characters} + assert_equal __(true), a == b + assert_equal __(true), a == c + end + + def test_flexible_quotes_can_handle_multiple_lines + long_string = %{ +It was the best of times, +It was the worst of times. +} + assert_equal __(54), long_string.length + assert_equal __(3), long_string.lines.count + end + + def test_here_documents_can_also_handle_multiple_lines + long_string = < 0, :black => 30, :red => 31, + :green => 32, :yellow => 33, :blue => 34, + :magenta => 35, :cyan => 36, + } + + module_function + + COLORS.each do |color, value| + module_eval "def #{color}(string); colorize(string, #{value}); end" + module_function color + end + + def colorize(string, color_value) + if use_colors? + color(color_value) + string + color(COLORS[:clear]) + else + string + end + end + + def color(color_value) + "\e[#{color_value}m" + end + + def use_colors? + return false if ENV['NO_COLOR'] + if ENV['ANSI_COLOR'].nil? + ! using_windows? + else + ENV['ANSI_COLOR'] =~ /^(t|y)/i + end + end + + def using_windows? + File::ALT_SEPARATOR + end + end + + class Sensei + attr_reader :failure, :failed_test, :pass_count + + in_ruby_version("1.8") do + AssertionError = Test::Unit::AssertionFailedError + end + + in_ruby_version("1.9") do + if defined?(MiniTest) + AssertionError = MiniTest::Assertion + else + AssertionError = Test::Unit::AssertionFailedError + end + end + + def initialize + @pass_count = 0 + @failure = nil + @failed_test = nil + @observations = [] + end + + PROGRESS_FILE_NAME = '.path_progress' + + def add_progress(prog) + @_contents = nil + exists = File.exists?(PROGRESS_FILE_NAME) + File.open(PROGRESS_FILE_NAME,'a+') do |f| + f.print "#{',' if exists}#{prog}" + end + end + + def progress + if @_contents.nil? + if File.exists?(PROGRESS_FILE_NAME) + File.open(PROGRESS_FILE_NAME,'r') do |f| + @_contents = f.read.to_s.gsub(/\s/,'').split(',') + end + else + @_contents = [] + end + end + @_contents + end + + def observe(step) + if step.passed? + @pass_count += 1 + if @pass_count > progress.last.to_i + @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.") + end + else + @failed_test = step + @failure = step.failure + add_progress(@pass_count) + @observations << Color.red("#{step.koan_file}##{step.name} has damaged your karma.") + throw :edgecase_exit + end + end + + def failed? + ! @failure.nil? + end + + def assert_failed? + failure.is_a?(AssertionError) + end + + def instruct + if failed? + @observations.each{|c| puts c } + encourage + guide_through_error + a_zenlike_statement + show_progress + else + end_screen + end + end + + def show_progress + bar_width = 50 + total_tests = EdgeCase::Koan.total_tests + scale = bar_width.to_f/total_tests + print Color.green("your path thus far [") + happy_steps = (pass_count*scale).to_i + happy_steps = 1 if happy_steps == 0 && pass_count > 0 + print Color.green('.'*happy_steps) + if failed? + print Color.red('X') + print Color.cyan('_'*(bar_width-1-happy_steps)) + end + print Color.green(']') + print " #{pass_count}/#{total_tests}" + puts + end + + def end_screen + if EdgeCase.simple_output + boring_end_screen + else + artistic_end_screen + end + end + + def boring_end_screen + puts "Mountains are again merely mountains" + end + + def artistic_end_screen + "JRuby 1.9.x Koans" + ruby_version = "(in #{'J' if defined?(JRUBY_VERSION)}Ruby #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION})" + ruby_version = ruby_version.side_padding(54) + completed = <<-ENDTEXT + ,, , ,, + : ::::, :::, + , ,,: :::::::::::::,, :::: : , + , ,,, ,:::::::::::::::::::, ,: ,: ,, + :, ::, , , :, ,::::::::::::::::::, ::: ,:::: + : : ::, ,:::::::: ::, ,:::: + , ,::::: :,:::::::,::::, + ,: , ,:,,: ::::::::::::: + ::,: ,,:::, ,::::::::::::, + ,:::, :,,::: ::::::::::::, + ,::: :::::::, Mountains are again merely mountains ,:::::::::::: + :::,,,:::::: :::::::::::: + ,:::::::::::, ::::::::::::, + :::::::::::, ,:::::::::::: +::::::::::::: ,:::::::::::: +:::::::::::: Ruby Koans ::::::::::::, +::::::::::::#{ ruby_version },::::::::::::, +:::::::::::, , :::::::::::: +,:::::::::::::, brought to you by ,,::::::::::::, +:::::::::::::: ,:::::::::::: + ::::::::::::::, ,::::::::::::: + ::::::::::::, EdgeCase Software Artisans , :::::::::::: + :,::::::::: :::: ::::::::::::: + ,::::::::::: ,: ,,:::::::::::::, + :::::::::::: ,::::::::::::::, + :::::::::::::::::, :::::::::::::::: + :::::::::::::::::::, :::::::::::::::: + ::::::::::::::::::::::, ,::::,:, , ::::,::: + :::::::::::::::::::::::, ::,: ::,::, ,,: :::: + ,:::::::::::::::::::: ::,, , ,, ,:::: + ,:::::::::::::::: ::,, , ,:::, + ,:::: , ,, + ,,, +ENDTEXT + puts completed + end + + def encourage + puts + puts "The Master says:" + puts Color.cyan(" You have not yet reached enlightenment.") + if ((recents = progress.last(5)) && recents.size == 5 && recents.uniq.size == 1) + puts Color.cyan(" I sense frustration. Do not be afraid to ask for help.") + elsif progress.last(2).size == 2 && progress.last(2).uniq.size == 1 + puts Color.cyan(" Do not lose hope.") + elsif progress.last.to_i > 0 + puts Color.cyan(" You are progressing. Excellent. #{progress.last} completed.") + end + end + + def guide_through_error + puts + puts "The answers you seek..." + puts Color.red(indent(failure.message).join) + puts + puts "Please meditate on the following code:" + if assert_failed? + puts embolden_first_line_only(indent(find_interesting_lines(failure.backtrace))) + else + puts embolden_first_line_only(indent(failure.backtrace)) + end + puts + end + + def embolden_first_line_only(text) + first_line = true + text.collect { |t| + if first_line + first_line = false + Color.red(t) + else + Color.cyan(t) + end + } + end + + def indent(text) + text = text.split(/\n/) if text.is_a?(String) + text.collect{|t| " #{t}"} + end + + def find_interesting_lines(backtrace) + backtrace.reject { |line| + line =~ /test\/unit\/|edgecase\.rb|minitest/ + } + end + + # Hat's tip to Ara T. Howard for the zen statements from his + # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html) + def a_zenlike_statement + if !failed? + zen_statement = "Mountains are again merely mountains" + else + zen_statement = case (@pass_count % 10) + when 0 + "mountains are merely mountains" + when 1, 2 + "learn the rules so you know how to break them properly" + when 3, 4 + "remember that silence is sometimes the best answer" + when 5, 6 + "sleep is the best meditation" + when 7, 8 + "when you lose, don't lose the lesson" + else + "things are not what they appear to be: nor are they otherwise" + end + end + puts Color.green(zen_statement) + end + end + + class Koan + include Test::Unit::Assertions + + attr_reader :name, :failure, :koan_count, :step_count, :koan_file + + def initialize(name, koan_file=nil, koan_count=0, step_count=0) + @name = name + @failure = nil + @koan_count = koan_count + @step_count = step_count + @koan_file = koan_file + end + + def passed? + @failure.nil? + end + + def failed(failure) + @failure = failure + end + + def setup + end + + def teardown + end + + def meditate + setup + begin + send(name) + rescue StandardError, EdgeCase::Sensei::AssertionError => ex + failed(ex) + ensure + begin + teardown + rescue StandardError, EdgeCase::Sensei::AssertionError => ex + failed(ex) if passed? + end + end + self + end + + # Class methods for the EdgeCase test suite. + class << self + def inherited(subclass) + subclasses << subclass + end + + def method_added(name) + testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s + end + + def end_of_enlightenment + @tests_disabled = true + end + + def command_line(args) + args.each do |arg| + case arg + when /^-n\/(.*)\/$/ + @test_pattern = Regexp.new($1) + when /^-n(.*)$/ + @test_pattern = Regexp.new(Regexp.quote($1)) + else + if File.exist?(arg) + load(arg) + else + fail "Unknown command line argument '#{arg}'" + end + end + end + end + + # Lazy initialize list of subclasses + def subclasses + @subclasses ||= [] + end + + # Lazy initialize list of test methods. + def testmethods + @test_methods ||= [] + end + + def tests_disabled? + @tests_disabled ||= false + end + + def test_pattern + @test_pattern ||= /^test_/ + end + + def total_tests + self.subclasses.inject(0){|total, k| total + k.testmethods.size } + end + end + end + + class ThePath + def walk + sensei = EdgeCase::Sensei.new + each_step do |step| + sensei.observe(step.meditate) + end + sensei.instruct + end + + def each_step + catch(:edgecase_exit) { + step_count = 0 + EdgeCase::Koan.subclasses.each_with_index do |koan,koan_index| + koan.testmethods.each do |method_name| + step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1) + yield step + end + end + } + end + end +end + +END { + EdgeCase::Koan.command_line(ARGV) + EdgeCase::ThePath.new.walk +} diff --git a/src/example_file.txt b/src/example_file.txt new file mode 100644 index 000000000..ffe7cbd89 --- /dev/null +++ b/src/example_file.txt @@ -0,0 +1,4 @@ +this +is +a +test diff --git a/src/first_test.rb b/src/first_test.rb new file mode 100644 index 000000000..708baf17a --- /dev/null +++ b/src/first_test.rb @@ -0,0 +1,11 @@ +require 'test/unit' + +class TestSomething < Test::Unit::TestCase + def test_assert + assert true + assert_equal 1, 1 + assert_equal 1, 1.0 + end +end + + diff --git a/src/path_to_enlightenment.rb b/src/path_to_enlightenment.rb new file mode 100644 index 000000000..64621bf4f --- /dev/null +++ b/src/path_to_enlightenment.rb @@ -0,0 +1,38 @@ +# The path to Ruby Enlightenment starts with the following: + +$LOAD_PATH << File.dirname(__FILE__) + +require 'about_asserts' +require 'about_nil' +require 'about_objects' +require 'about_arrays' +require 'about_array_assignment' +require 'about_hashes' +require 'about_strings' +require 'about_symbols' +require 'about_regular_expressions' +require 'about_methods' +require 'about_constants' +require 'about_control_statements' +require 'about_true_and_false' +require 'about_triangle_project' +require 'about_exceptions' +require 'about_triangle_project_2' +require 'about_iteration' +require 'about_blocks' +require 'about_sandwich_code' +require 'about_scoring_project' +require 'about_classes' +require 'about_open_classes' +require 'about_dice_project' +require 'about_inheritance' +require 'about_modules' +require 'about_scope' +require 'about_class_methods' +require 'about_message_passing' +require 'about_proxy_object_project' +require 'about_to_str' +in_ruby_version("jruby") do + require 'about_java_interop' +end +require 'about_extra_credit' diff --git a/src/test_helper.rb b/src/test_helper.rb new file mode 100644 index 000000000..9accf96d8 --- /dev/null +++ b/src/test_helper.rb @@ -0,0 +1,7 @@ +require 'test/unit' + +def __ + "FILL ME IN" +end + +EdgeCase = Test::Unit diff --git a/src/triangle.rb b/src/triangle.rb new file mode 100644 index 000000000..ef4859bac --- /dev/null +++ b/src/triangle.rb @@ -0,0 +1,28 @@ +# Triangle Project Code. + +# Triangle analyzes the lengths of the sides of a triangle +# (represented by a, b and c) and returns the type of triangle. +# +# It returns: +# :equilateral if all sides are equal +# :isosceles if exactly 2 sides are equal +# :scalene if no sides are equal +# +# The tests for this method can be found in +# about_triangle_project.rb +# and +# about_triangle_project_2.rb +# +def triangle(a, b, c) + # WRITE THIS CODE + #-- + a, b, c = [a, b, c].sort + fail TriangleError if (a+b) <= c + sides = [a, b, c].uniq + [nil, :equilateral, :isosceles, :scalene][sides.size] + #++ +end + +# Error class used in part 2. No need to change this code. +class TriangleError < StandardError +end