21-01-2022
Meta Stuff and Todos
pry-debug
surge
bash script to move and rename file
the about file, resources file, and images for Zola
Today's Foci
Focus 1
- What/How? See if I can write and execute a good plan for the airport
- Measure - how far did I stray from the plan?
Focus 2
- Do the airport challenge, balance note taking and productivity
- Complete more than eighty percent of challenge AND take notes on ruby - 6+ detailed bits of info below by end of day in tactics/objects/methods/gems
Topic
The Airport Challenge
if you refer to another solution, link it in your README.md
if you have a partial solution, check in a partial solution
must submit a pull by 09:00 24-01-2022
Instructions
fork this repo, and clone to my local machine
run gem install bundler (if not installed)
We have a request from a client to write the software to control the flow of planes at an airport. The planes can land and take off provided that the weather is sunny. Occasionally it may be stormy, in which case no planes can land or take off. Here are the user stories that we worked out in collaboration with the client:
Must use a random number generator to set the weather
Try to defend against edge cases - inconsistent states
- Planes only take off from airports they are in
- Planes flying cannot take off or be in an airport
- Planes landed cannot land again, and must be in an airport
to TDD RNG - look at the die case
Measure
-
All tests pass
-
High percentage test coverage
-
elegant code - class has purpose, methods short - SRP, Unix style!
Bonus
- RSpec feature test that lands and takes off a number of planes
User Stories
As an air traffic controller So I can get passengers to a destination I want to instruct a plane to land at an airport
As an air traffic controller So I can get passengers on the way to their destination I want to instruct a plane to take off from an airport and confirm that it is no longer in the airport
As an air traffic controller To ensure safety I want to prevent landing when the airport is full
As the system designer So that the software can be used for many different airports I would like a default airport capacity that can be overridden as appropriate
As an air traffic controller To ensure safety I want to prevent takeoff when weather is stormy
As an air traffic controller To ensure safety I want to prevent landing when weather is stormy
User Story Table
Obj | Msg |
---|---|
Plane | land |
take_off | |
Airport | check_hanger |
hanger_capacity | |
check_safe_weather | |
Weather | is_stormy |
Challenge
SRP, KISS! Let's goooo!!!
First, I think two objects will do
Airport, and Plane.
Plane has a boolean value, in_flight, default false.
Plane.take_off, only if in_flight false, in_flight=true
Plane.land, if only in_flight true, in_flight=false
Call Airport.land_plane
if Obj.in_flight=true
Obj_flight
end
end
Class - plane.rb
dont do it T!!! tests first!!!
Error 1
remember to '/bin/bash --login' when I'm using rvm
didn't check ma rooby version. derp.
( In my defence, thought i was on 3.1.0 )
success! we made tests and built a plane around them!
./spec/plane_spec.rb
require './lib/plane'
describe Plane do
let(:plane) { Plane.new }
it '#can take off' do
expect(plane).to respond_to(:take_off)
end
it '#can land' do
expect(plane).to respond_to(:land)
end
it '#won\'t take off if in flight' do
plane.take_off
expect { plane.take_off }.to raise_error("This plane is already in flight - it cannot take off")
end
it '#won\'t land if grounded' do
expect { plane.land }.to raise_error("This plane is grounded, and doesn't need to land")
end
end
./lib/plane.rb
class Plane
def initialize(in_flight = false)
@in_flight = in_flight
end
def take_off
return raise ("This plane is already in flight - it cannot take off") unless @in_flight == false
@in_flight = true
end
def land
return raise ("This plane is grounded, and doesn't need to land") unless @in_flight == true
@in_flight = false
end
end
Fantastic! next, airport tests and airport class
I'm moving the weather class to become a method on the airport
check_hanger
- are the planes in the hanger
- are the expected amount of planes in the hanger
- if hanger is full, don't allow planes in
- hanger to only take plane objects?
give the plane id_attr? for persistence? attr_accessor :plane_id
init (plane_id, in_flight=false)
@plane_id = plane_id
Try to use smaller steps, dont jump ahead
first error uninitialized contant Airport - good! there's nothing in the airport.rb yet
Airport class made
now the tests fail properly (failing upward) :)
expect(airport.hanger).to eq [ ]
I'm a little sad about the above - i think I messed up trying to look up assigns in RSpec
I'm thinking a hanger should perhaps have the .freeze method, but I'm unsure. Then you would add multiple hangers, when necessary.
Answer: Step 1, always check versions / lock files
I had a problem implementing the random tests initially, but they should look something like this
allow(INSTANCE).to receive(METHOD).and_return(VALUE)
thanks to a fellow HOOM'N, I learned how to set a Random.new value that is within a Class's method scope, similiar to above:
allow(INSTANCE).to receive(:rand).and_return(VALUE)
I am grateful to that HOOM'N, as I would otherwise have been stuck for a very, very long time.
I then made a foolish error, that ate Friday afternoon and Saturday morning - a typo
RSpec kword raise_error requires braces { }, not parentheses.
allow(airport).to receive(:rand).and_return(6)
expect{airport.take_off}.to raise_error "There is a storm..."
whoops...
I have also been debating about how to implement the weather - is it a different class? it could be - but a guard clause will do the job.
def check_weather
return "stormy" unless rand(1..6) <= 5
"sunny"
end
I'm also uncertain of using multiple guard clauses within a single method. But it works, and isn't necessarilly unclear?
anywhere, here's the airport code and airport test. I think I've over-tested? I'm uncertain. The tests look modular, but sometimes they overlap - do I really need to use the respond_to method after a certain point? Should I swap it out? I don't know
./spec/airport_spec.rb
require './lib/airport'
describe Airport do
let(:airport) { Airport.new }
let(:plane) { Plane.new }
let(:flying_plane) { Plane.new(in_flight = true) }
# ==== HANGER TESTS ====
it '#has a hanger' do
expect(airport.hanger.class).to be Array
end
it '#is possible to check the hanger' do
expect(airport).to respond_to(:check_hanger)
end
it '#is possible to modify the hanger size' do
expect(airport).to respond_to(:modify_hanger)
end
it '#s hanger has a standard capacity of 40' do
expect(airport.check_hanger).to eq 40
end
it '#can be made with different hanger size' do
airport_big = Airport.new(80)
expect(airport_big.check_hanger).to eq 80
end
it '#s hanger size can be modified after initialization' do
airport.modify_hanger(100)
expect(airport.check_hanger).to eq 100
end
# ==== LANDING/TAKEOFF TESTS ====
it '#landing planes can get to the hanger' do
allow(airport).to receive(:rand).and_return(5)
airport.land_plane(flying_plane)
expect(airport.hanger).to eq([flying_plane])
end
it '#won\'t let planes land if the hanger is full' do
small_airport = Airport.new(hanger_capacity = 3)
allow(small_airport).to receive(:check_weather) { "sunny" }
3.times { small_airport.land_plane(Plane.new(in_flight = true)) }
expect { small_airport.land_plane(flying_plane) }.to raise_error("Hanger is full")
end
it '#won\'t let a plane take off, if it isn\'t in the airport' do
allow(airport).to receive(:rand).and_return(5)
expect { airport.take_off(plane) }.to raise_error("#{plane} not in hanger")
end
it '#let\'s a plane take off if its in the hanger' do
allow(airport).to receive(:rand).and_return(5)
airport.hanger << plane
expect(airport.take_off(plane)).to eq plane
end
it '#confirms plane has left the airport' do
allow(airport).to receive(:rand).and_return(5)
airport.hanger << plane
airport.take_off(plane)
expect(airport.contains_plane?(plane)).to eq("#{plane} departure confirmed")
end
# ==== WEATHER TESTS ====
it '#can check local weather' do
expect(airport.check_weather.class).to be String
end
it '#returns stormy if the weather is stormy' do
allow(airport).to receive(:rand) { 6 }
expect(airport.check_weather).to eq "stormy"
end
it '#returns sunny if the weather is sunny' do
allow(airport).to receive(:rand) { 5 }
expect(airport.check_weather).to eq "sunny"
end
it '#won\'t allow planes to take off if weather is stormy' do
airport.hanger << plane
# srand(1)
allow(airport).to receive(:rand).and_return(6)
expect { airport.take_off(plane) }.to raise_error "There is a storm - no planes can take off or land"
end
it '#won\'t allow planes to land if weather is stormy' do
allow(airport).to receive(:rand).and_return(6)
# srand(1)
expect { airport.land_plane(flying_plane) }.to raise_error "There is a storm - no planes can take off or land"
end
end
./lib/airport.rb
class Airport
STORM_ERROR = "There is a storm - no planes can take off or land".freeze
attr_reader :hanger
def initialize(hanger_capacity = 40)
@hanger = []
@hanger_capacity = hanger_capacity
end
def check_hanger
@hanger_capacity
end
def modify_hanger(new_capacity)
@hanger_capacity = new_capacity
end
def land_plane(plane)
return raise "Hanger is full" unless @hanger.length < @hanger_capacity
return raise STORM_ERROR unless check_weather != "stormy"
plane.land
@hanger << plane
end
def take_off(plane)
return raise "#{plane} not in hanger" unless @hanger.include?(plane)
return raise STORM_ERROR unless check_weather != "stormy"
plane.take_off
@hanger.delete(plane)
end
def contains_plane?(plane)
return "#{plane} in airport" if @hanger.include?(plane)
"#{plane} departure confirmed"
end
def check_weather
return "stormy" unless rand(1..6) <= 5
"sunny"
end
end
Notes
to exit git log, press 'q' or 'z'
User Story 'as an' identifies a user of the system, maybe not ab object?
look up RSpec assigns
Rspec allow(instance).to receive(:method-sym) { value }
will expect pretend the method returns that value when called
Useful Tactics
- vi / vim can find move to line_number n with commandmode :n
Ruby Objects and Methods Learned Today
Objects
DateTime.new
Ruby docs, the DateTime object
DateTime object is created using
- DateTime::new
- DateTime::jd
- DateTime::ordinal
- DateTime::commercial
- DateTime::parse
- DateTime::strptime
- DateTime::now
- Time#to_datetime
- ...
- and more!
require 'date'
DateTime.new(2001,2,3,4,5,6)
output: #<DateTime: 2001-02-03T04:05:06+00:00 ...>
The last element of day, hour, minute, or second can be fractional
require date
DateTime.new(2001,2,3.5)
output: #<DateTime: 2001-02-03T04:05:06+03:00 ...>
To offset time by a specific amount
require date
# Example 1
DateTime.new(2001,2,3,4,5,6,Rational(3,24))
# or...
# Example 2
DateTime.new(2001,2,3,4,5,6, '+03:00')
output: #<DateTime: 2001-02-03T04:05:06+03:00 ...>
DateTime can take a (start) parameter, denoting a day number, which should be 2298874
The default start parameter is Date:: ITALY(2299161=1582-10-15)
Methods
RSpec
expect(object).to be_instance_of Class
Gems Learned Today
Sharing Explanation Examples
Making Plane Instances
new plane-instance (a real plane) is made when
Plane.new
is used
an instance, or real plane, has been made! ace! but we didn't store it anywhere - it span off into the Aether of Ruby!
maybe we want to store that plane somewhere, in a variable, for later use?
my_favourite_plane = Plane.new
now, we can access my_favourite_plane - and it will always be the same plane instance or the same real plane. It will always be my_favourite_plane (unless we forcibly change it later!) remember, we can only access my_favourite_plane, while its in the same scope
I might create a function to release_a_plane, as below;
def release_a_plane
end
it isn't doing anything at the minute. There are no planes to release! so in the release a plane method i might do
1. def release_a_plane
2. my_favourite_plane = Plane.new
3. return my_favourite_plane
4. end
but still we have a problem - my_favourite_plane doesnt exist outside of line 1 - 4. If we wanted to use my_favourite_plane on line 16 - we can't use my_favourite_plane
Thankfully, we can solve this!
on line 16 we want a plane; maybe my_favourite_plane wants to fly? Even assuming we've made a perfectly functional method called 'fly'
...
14.
15.
16. my_favourite_plane.fly
my_favourite_plane doesn't exist on line 16.
my_favourite_plane is not going to fly.
But we do have access to a method, and that method is called 'release_a_plane'.
And in release_a_plane, we are making and returning a real plane - a plane-instance. When we use a method - if it works - the method, kind of becomes it's return value
Were we to use:
14. my_favourite_plane = release_a_plane
15.
16. my_favourite_plane.fly
All of a sudden, it works! We take to the skies!
a better coded version of the above might be something like
1. def release_a_plane
2. return Plane.new
3. end
...
14. my_favourite_plane = release_a_plane
15.
16. my_favourite_plane.fly