21-01-2022

1286 words 7 minutes

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

  1. What/How? See if I can write and execute a good plan for the airport
  2. Measure - how far did I stray from the plan?

Focus 2

  1. Do the airport challenge, balance note taking and productivity
  2. 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

The Airport Challenge, Github

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

ObjMsg
Planeland
take_off
Airportcheck_hanger
hanger_capacity
check_safe_weather
Weatheris_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