Skip to content

Instantly share code, notes, and snippets.

@StandardGiraffe
Last active January 9, 2023 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StandardGiraffe/b946bc787def30807fe2f68de846d0c3 to your computer and use it in GitHub Desktop.
Save StandardGiraffe/b946bc787def30807fe2f68de846d0c3 to your computer and use it in GitHub Desktop.
class CondimentJar
class ContainerClosedError < StandardError; end
class ContainerEmptyError < StandardError; end
attr_accessor :contents
def initialize(contents = nil)
@contents = contents # Jar is empty by default.
@state_of_the_lid = :closed # I wasn't raised in a barn.
end
def empty?
contents.nil?
end
# All jars contain one portion for simplicity.
def has_stuff?
!empty?
end
def closed?
@state_of_the_lid == :closed
end
def close!
@state_of_the_lid = :closed
end
def open?
@state_of_the_lid == :open
end
def open!
@state_of_the_lid = :open
end
def relinquish_condiment!
if closed?
raise ContainerClosedError, "The jar is closed and knife-impermeable."
end
if empty?
raise ContainerEmptyError, "The jar is empty. How disappointing."
end
# Provide the contents and empty the container
return contents.tap { |t| self.contents = nil }
end
end
class Knife
class DirtyKnifeError < StandardError; end
class EmptyKnifeError < StandardError; end
class InvalidKnifeError < StandardError; end
class InvalidTargetError < StandardError; end
attr_accessor :contents
def initialize
@contents = nil
end
def clean?
contents.nil?
end
def clean!
contents = nil
end
def loaded?
!clean?
end
def load_from!(container)
if loaded?
raise DirtyKnifeError, "This knife is already loaded. Don't mix your condiments!"
end
@contents = container.relinquish_condiment!
end
def smear!(bread_surface)
unless loaded?
raise EmptyKnifeError, "This knife is too unbesmirched by condiment to smear anything! Load it first."
end
unless bread_surface.is_a?(SliceOfBread::Surface)
raise InvalidTargetError, "#{bread_surface} isn't the surface of a slice of bread! Put the knife down!"
end
bread_surface.be_smeared!(self)
end
end
class SliceOfBread
class Surface
class AlreadySmearedError < StandardError; end
attr_accessor :contents
def initialize
@contents = nil
end
def plain?
contents.nil?
end
def smeared?
!plain?
end
def be_smeared!(knife)
unless knife.is_a?(Knife)
raise ::Knife::InvalidKnifeError, "That's not hygienic."
end
unless plain?
raise AlreadySmearedError, "This surface was already smeared!"
end
unless knife.loaded?
raise ::Knife::EmptyKnifeError, "This knife is still too clean to smear with. How did this even happen...?"
end
self.contents = knife.contents
knife.contents = nil
end
end
attr_reader :top
attr_reader :bottom
def initialize
@top = Surface.new
@bottom = Surface.new
end
def plain?
top.plain? and bottom.plain?
end
def smeared?
top.smeared? or bottom.smeared?
end
end
class Sandwich
class NotEnoughSlicesError < StandardError; end
class OutsideSmearedError < StandardError; end
class AlreadyBuiltError < StandardError; end
class TooPlainError < StandardError; end
class ImmatureSandwichError < StandardError; end
class AlreadyCutError < StandardError; end
# These errors already exist in the Knife and SliceOfBread namespaces respectively; I feel okay about adding them again here because I might want to add specific code to these versions in the future.
class InvalidKnifeError < StandardError; end
class DirtyKnifeError < StandardError; end
attr_reader :slices
def initialize(*slices_of_bread)
@slices = slices_of_bread || [ ]
@built = false
@cut = false
end
def flavours
slices.flat_map do |slice|
[ slice.bottom.contents, slice.top.contents ]
end.uniq.compact
end
def flavours_human_readable
f = flavours.map(&:downcase)
if f.count == 2
return f.join(' and ')
end
f[-1] = 'and ' + f[-1]
f.join(', ')
end
def <<(slice)
@slices << slice
end
def ready_to_eat?
@built and @cut
end
def build!
if @built
raise AlreadyBuiltError, "It's already a glorious tower of food!"
end
if slices.count < 2
raise NotEnoughSlicesError, "#{slices.length} slices of bread does not a sandwich make."
end
unless slices.first.bottom.plain? and slices.last.top.plain?
raise OutsideSmearedError, "This sandwich would be icky to hold."
end
# Check all but the top slice for plainness
if slices[..-2].map(&:plain?).any?(true)
raise TooPlainError, "This sandwich might actually be a loaf."
end
@built = true
end
def cut!(knife)
unless @built
raise ImmatureSandwichError, "Build the sandwich and then cut it in one glorious stroke."
end
unless knife.is_a?(Knife)
raise InvalidKnifeError, "That's not hygienic."
end
unless knife.clean?
raise DirtyKnifeError, "No! You'll get the edge all yucky with that knife."
end
if @cut
raise AlreadyCutError, "One cut will do."
end
@cut = true
end
end
class Chef
class TakenForGrantedError < StandardError; end
class UnspeakablyBlandError < StandardError; end
def self.number_of_slices_for(number_of_requested_flavours)
1 + (number_of_requested_flavours / 2.0).ceil
end
def initialize(condiments: [], knife: nil, slices_of_bread: [])
@condiments = condiments
@knife = knife
@slices_of_bread = slices_of_bread
@under_appreciated = true
end
def please # Courtesy is important
@under_appreciated = false
self
end
def make_me_a_sandwich!(*requested_flavours)
# === Gate Conditions ====================================================
if @under_appreciated # Courtesy is important!
raise TakenForGrantedError, "Poof: you're a sandwich."
end
@under_appreciated = true # You get ONE request per courtesy.
# No vacuum- or integer-sandwiches please; they give me a headache.
requested_flavours.keep_if { |flavour| flavour.is_a? String }
if requested_flavours.count < 1
raise UnspeakablyBlandError, "I think you might just want some dry toast."
end
condiments = opened_condiment_jars_for(requested_flavours)
sandwich_bread = plain_slices_for(requested_flavours.count)
knife = readied_knife
# Explode the bread into surfaces:
sandwich_bread_surfaces = sandwich_bread.map do |slice|
[ slice.bottom, slice.top ]
end.flatten
# Skipping the bottom surface, apply condiments until we're out of condiments!
sandwich_bread_surfaces[1..].zip(condiments).each do | surface, condiment |
break if !condiment
knife.load_from!(condiment)
knife.smear!(surface)
end
# The magic happens
sandwich = Sandwich.new(*sandwich_bread)
sandwich.build!
sandwich.cut!(knife)
if sandwich.ready_to_eat? # How could it not be? Still.
puts "Finit. One #{sandwich.flavours_human_readable} sandwich; bon appétit!"
return sandwich # Needlessly explicit "return"; assume to be flourish.
else
puts "Hmm. Something went wrong despite my genius."
end
end
private
def readied_knife
ensure_knife
@knife.clean!
@knife
end
def opened_condiment_jars_for(requested_flavours)
# Insurance:
ensure_condiments_are_actually_condiments
ensure_condiments_are_available(requested_flavours)
# Array Building:
requested_flavours.map do |flavour|
condiment = @condiments.find { |jar| jar.contents == flavour}
condiment.open!
condiment
end
end
def plain_slices_for(number_of_requested_flavours)
ensure_bread_is_actually_bread
ensure_enough_bread_is_available(number_of_requested_flavours)
# #slice! will remove the specified range of elements from the host array and return them as a new array. The "- 1" accounts for Ruby arrays being zero-indexed.
@slices_of_bread.slice!(
0..(Chef.number_of_slices_for(number_of_requested_flavours) - 1)
)
end
# Throw out any empty bottles and/or cans of paint.
def ensure_condiments_are_actually_condiments
@condiments.keep_if do |condiment|
condiment.is_a?(CondimentJar) and condiment.has_stuff?
end
end
# Gather any missing condiments...
def ensure_condiments_are_available(requested_flavours)
missing_condiments = [ ]
# Let's avoid multiple trips to the store by taking stock before we go.
requested_flavours.each do |flavour|
unless @condiments.find { |jar| jar.contents == flavour}
missing_condiments << flavour
end
end
if missing_condiments.any?
puts "Looks like we could use a few things from the store..."
missing_condiments.each do |condiment|
@condiments << CondimentJar.new(condiment)
puts "Bought me some #{condiment}"
end
puts "There we go. That's better."
end
end
# Safety first.
def ensure_knife
return if @knife.is_a?(Knife)
puts "I'll probably want a knife for this..."
@knife = Knife.new
puts "... Full tang mokume gane butter knife. Perfect."
end
# Keep our existing bread collection sane and unsullied.
def ensure_bread_is_actually_bread
@slices_of_bread.keep_if do |slice|
slice.is_a?(SliceOfBread) and slice.plain?
end
end
# Make sure we won't run out in the middle of the operation.
def ensure_enough_bread_is_available(number_of_requested_flavours)
sufficient_slices = Chef.number_of_slices_for(number_of_requested_flavours)
return if @slices_of_bread.count >= sufficient_slices
puts "This isn't enough bread to make your sandwich. Let me just grab some more."
while @slices_of_bread.count < sufficient_slices
@slices_of_bread << SliceOfBread.new
puts "Yoink!"
end
puts "... There we go. Pile o' bread!"
end
end
daddy = Chef.new
lunch = daddy.please.make_me_a_sandwich!("Relish", "Marmite", "Nutella™", "Sriracha")
pp lunch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment