-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coding challenge #23
base: master
Are you sure you want to change the base?
Coding challenge #23
Conversation
Give folks the ability to use my awesome ideas :D
…itself, and its description.
Masochism and constant nagging await
Allow spec files to span as many lines as they need to. Skip testing coffee_place executable.
Big whoops - apparently latitude is the North-South one :D
This makes rspec output too verbose. Uncomment this line only when debugging slow test times.
Maybe it was the wind...
I would like to give a shoutout to all other coding challenge participants in the past that also forgot to include info about setup or running the code. Y'all are the real MVP. PS: Only looked at previous PRs on this repo after already submitting mine. This is just preemptive fixing :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for submitting. Cool solution overall. Have some questions that I would like to hear your thoughts on.
lib/coffee_place/cli.rb
Outdated
|
||
def validate_user_location!(cli_opts) | ||
lat, lon = cli_opts.remaining | ||
location_result = Location.validate(lat: lat, lon: lon, name: 'Current user') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reasoning behind the validate method to return, in case of success, an instance of CoffeePlace::Location
? From the naming, I'd not expect it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this method does more than what is expected.
I'll try to refactor this part and push another commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: fixed by 482136f9
lib/coffee_place/location.rb
Outdated
location = new(lat: result_lat.value, lon: result_lon.value, name: name.to_s) | ||
|
||
return Result.failure("Invalid latitude: #{lat}") unless location.valid_latitude? | ||
return Result.failure("Invalid longitude: #{lon}") unless location.valid_longitude? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is your take on multiple return statements? Is it good / bad, why, when should one use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^I've the same question. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like using guard clauses - it is the only place in Ruby where I use the return
statment.
Basically the success path travels down the method, while guard clauses will end control flow prematurely
in case of an unexpected input, or some precondition not met.
Here, if the location is invalid, which should be unlikely, I fail-fast on the first error and return it.
Another approach would be to have an @errors
array, in which to collect errors.
I would then only check the errors at the end of the method and use Ruby's implicit expression return value
to return either the errors -if there are any, or the success value.
I think both approaches are valid.
I like Avdi Grimm's Confident Ruby book quite alot.
He uses guard clauses in order to clean up methods with branching logic in pretty neat ways.
# frozen_string_literal: true | ||
|
||
module CoffeePlace | ||
Result = Struct.new(:success?, :value, :error) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you decide when to return instances of this class from a method? Seeing it is used across the solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Ruby, predicate methods return true
/ false
, while finder methods return some truthy value
/ nil.
I find that in a lot of situations a Result
type, with two variants - Success(value)
, and Failure(error)
comes in handy. This is common in many functional languages, but also useful in OOP, when the return value you need
has to be able to represent different variants.
A really nice gem for handling this across an application is dry-monads
You don't have to go heavy on the FP. Just using variant types consistently, when neither a predicate nor a finder
does the job is sufficient.
This also enables neat ways of assembling operations, as seen in dry-transaction
Note: dry-transaction
seems to be undergoing API changes, as it is not in a 1.0
version yet :D
I've used this gem in the past in a production application, validating user submitted Excel files, and it worked pretty nicely - the code was easy to extend and refactor.
|
||
This is an attempt at solving the closest coffee shop coding challenge. | ||
|
||
All good software starts with making flawed assumptions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that ain't the truth, I don't know what is! 😹
@locations | ||
.map { |location| [target_location.distance_to(location), location] } | ||
.sort_by(&:first) | ||
.take(num_results) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏿
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love Ruby's Enum
❤️
I also enjoy this style of chaining operations in Elixir.
While inspired by LISP, it uses a data-first
approach, placing the
emphasis on the data, and not on the functional transformation.
For JavaScript, I enjoy using the Ramda npm package.
Ramda puts emphasis on function composition though.
This makes it much harder to use and learn, in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Advanced solution overall, but a bit over engineered.
show_help_and_exit! if cli_opts.print_help? | ||
show_version_and_exit! if cli_opts.show_version? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious - why did you write up the methods with a bang?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like using a bang when naming methods that have important side effects.
This warns the programmer to be weary of side effects.
These methods exit the program - which is pretty severe, as far as side effects go.
In ActiveRecord
's design, most methods with a bang have the side effect of throwing errors.
(e.g. save!
, validate!
)
In Ruby's standard library, a bang usually means that a method mutates data in place,
instead of creating a new data structure - for example Array#map!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I imagined that's the reason. Thank you for answering. :)
require_relative './search' | ||
|
||
module CoffeePlace | ||
# Parses and runs CLI application using supplied args. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you feel the need to explain what the classes do through additional comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tldr; Rubocop is being naggy as always! 🤖 😛
Rubocop has a style cop that emits warnings when you haven't added a class comment description.
While disabling this cop is valid, when method names are explicit, I prefer using a consistent style.
Basically, some classes need no further explanation - their name is sufficient to understand their purpose.
Other classes need additional explanation as to their responsibility.
This would result in some classes having a documentation comment at the top, while others would
not have it - a minor style inconsistency.
This rubocop linter forces you to consistently add documentation at the top of any class,
making sure the style remains consistent.
I'm pretty sure the rubocop config for rails disables this, as convention over configuration makes
alot of class descriptions redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I apologise for writing you a short novel in response to your comment ... I don't know how to stop myself from rambling sometimes 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually disable that cop, I find it naggy too, but recently the thought occured to me that it can be much more useful in more complex projects, as it would help new developers understand the project implementation-wise more easily.
Out of curiosity - have you used this documentation cop in production? If so, has it helped the team understand the project better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually used the rails-rubocop config, which I think has this cop turned off.
What helped us understand new projects best were design documents and mockups,
which are initially created when first interacting with the client. These explain what the
project aims to be, which is usually more than its current implementation state.
Maybe including links to helpful documents in the project's own ReadMe would help
with onboarding on new projects. We usually just shared these on Slack :D
Comments at the top of more complicated methods, or specs in which these methods are
tested usually help clarify specific parts of the code - but its hard to build a 'big picture' only
from these.
class CLIOptions | ||
attr_accessor :version, :help, :remaining, :num_results | ||
|
||
def initialize(version: false, help: false, remaining: []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting use of flags. Generally, are there any issues that could arise from the use of boolean params?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally passing in nil
is always a major pain.
There is always the possibility of someone passing a String
too - such as 'false'
.
ActionControlller
usually does some light type casting to ensure values sent from checkboxes
or other fields are converted into boolean values.
When I use these sort of params internally in my code, I try to make sure that I always send a boolean,
even when the value is missing - by passing in a default value.
When using user submitted fields, some sort of type casting is useful.
I have found this StackOverflow answer helpful when parsing user submitted CSV files,
that contain columns which represent boolean values.
I would use something like:
module BooleanHelper
module_function
def cast(value)
ActiveModel::Type::Boolean.new.cast(value)
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This are all valid points, but there is also a code smell that arises from using bools in your methods, you can read more about it in this Martin Fowler article, dating way back in 2011
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more article recommendation from me: Clean code: The curse of a boolean parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that using boolean arguments for control flow leads to needless complexity.
Having simpler methods to handle each case is better.
Thank you for the two articles.
uri.open(&block) | ||
rescue OpenURI::HTTPError => e | ||
add_import_error "Failed to download remote file \"#{uri}\": #{e.message}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if an invalid URI is provided, e.g. http://asd.asd
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method from where with_remote_file
is called - with_source
has a rescue as well.
def with_source(source_name, &block)
uri = URI.parse(source_name)
# When we have an URL, download remote file
return with_remote_file(uri, &block) if uri.scheme
# When we have a local file, try to open it
return with_local_file(uri, &block) if File.exist?(uri.path)
add_import_error unknown_source_error_message(source_name)
rescue URI::InvalidURIError
add_import_error "#{source_name} is not a valid URL or local filename"
end
That URI::InvalidURIError
catches invalid URL's.
What I try to do here is to see if it is a parsable (i.e. valid) URI.
If it is an URI, then see if it has a scheme
- as all http
and https
URLs do.
If it is valid as an URI, but not also a URL, then blindly assume we are dealing with a file,
and try to load the CSV from that local file.
Ways this could horribly backfire: someone sends some URL with a different scheme,
e.g. a database scheme: redis://<some_ip>/whoops
This could potentially become a security vulnerability.
More sanity checks should be done on the input, before trying to open that uri.
In a real application, of course, I would place a todo at the top of the method:
# TODO: make sure we aren't hacked in a stupid way (commented by madalindaniel 11months ago) ...
😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, whenever you download some remote file to your server ... be weary!
The night is dark and full of errors. 👻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I didn't read the example you provided carefully the first time.
http://asd.asd
is not an invalid URI.
asd
is simply not yet defined as a top level domain.
I think uri.open
will result in a DNS
query trying to find the IP responsible for this domain, and fail,
resulting in a OpenURI::HTTPError
error being raised.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whelp, just tested it ... it throws a wonderful:
3.0.0/net/http.rb:987:in `initialize': getaddrinfo: Name or service not known (SocketError)
I am not catching that error, and it is bubbling through both rescue clauses.
I was hoping Ruby's net/http catches and re-throws these as OpenURI::HTTPError
, but it doesn't.
I'll add a few more rescue clauses to catch SocketError
as well ... sigh ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: fixed by 89e854, abb0fb09
Thank you for showing me an edge case - nice save!
Note: normally I would delete my previous 5 comments on this same issue ... but I've decided to leave them as is.
These comments show a journey of self discovery - from conceited self-assurence, to testing and understading
false assumptions, and finally fixing a bug. Thanks for setting me on this journey Alex 😸
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, no problem :D
lib/coffee_place/location.rb
Outdated
location = new(lat: result_lat.value, lon: result_lon.value, name: name.to_s) | ||
|
||
return Result.failure("Invalid latitude: #{lat}") unless location.valid_latitude? | ||
return Result.failure("Invalid longitude: #{lon}") unless location.valid_longitude? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^I've the same question. 😄
<<~ERROR | ||
Don't know how to import locations from: #{source_name.inspect} | ||
Make sure the URL or filename is not misstyped. | ||
ERROR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you proceed if we'd want to re-use this error message in 10 other places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tldr; constant or helper method
Long form answer
If the error message was static, I would define it as a constant, at the top of some helper module.
As this message requires a source_name
parameter, it has to become a method ... probably also on some helper module.
module ErrorHelper
module_function
def invalid_import_location(source_name)
<<~ERROR
Don't know how to import locations from: #{source_name.inspect}
Make sure the URL or filename is not misstyped.
ERROR
end
end
I have a thing for helper modules, don't I?! ... huh ...
They are kind of like singleton classes that don't bind any state 😛
On a serious note, it's easier to have one ErrorMessagesHelper
module with all of the error messages
used throughout a large application, than having to CTRL+F
the whole codebase trying to find were all
of these things are defined.
Alternative approach
I like the way the devise
uses localisation yml
files to keep all error messages in the same place,
and also allow for i18n
. So, using the i18n
gem + localisation files is also a valid option for a web app
Pointless side story containing my grumbling and complaints about past projects.
I worked on a React application in the past that had similar error messages within the render functions
of many components. To change some error message I would need to find the right component, change
the message, wait for the web-dev-server to reload, and pray that I changed the message in the right
file. It would become a CTRL+F
journey through many, many files - which was thoroughly annoying.
I tried moving all of the goddamn error messages as constants in the same vanilla JS, and importing
it from there to each component that needed it.
Something like
// within src/utils/error_messages.js
export const BAD_THINGS_HAPPENED = 'Oh noes!';
// within components, at the top
import { BAD_THINGS_HAPPENED } from '@/src/utils/error_messages.js'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to break the short novel I wrote in the previous comment, into multiple logical chapters.
Stay tuned for it's sequel!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My approach is usually to just extract them to a locales/errors/en.yml
or something similar and just rely on I18n.t
. Worked like a charm so far, you can do interpolation if needed too.
A similar approach is also used by the dry-validation gem
def success(value) | ||
new(true, value, nil) | ||
end | ||
|
||
def failure(error) | ||
new(false, nil, error) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting approach - what concept are you using here to describe happy/unhappy paths?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using a sum type, with two variants.
This is similar to Result
in Rust, or the Maybe
type from functional languages.
The happy path should always hold a helpful value.
The unhappy paths always returns an error message.
A better approach might be to have the failure
cases return any
object that responds to a message
method - allowing for various
ducked type error message objects to be passed in.
This would require string error messages to be wrapped in such an object,
which could happen automatically inside the failure
method here.
class Result
FailureWrapper = Struct.new(:message)
def self.failure(error)
error = FailureWrapper.new(error) unless error.respont_to?(:message)
new(false, nil, error)
end
end
I think that using string error messages for the failure cases is sufficient
for a small example app like this, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, basically you're describing monads 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Poor man's monads, without fmap
and chain/bind
.
But yes! :D
(I'm referring to the Result implementation in my code)
Also, using the dry-monads
gem consistently throughout multiple projects sounds nice.
It comes with alot of goodies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used it in a few projects, it worked quite great, especially when used across all the layers of the project, as errors could be wrapped in a Failure
either at the repository level or business layer and it would be passed along all the way to the presentation one where it would get handled appropriately, basically making it possible to have no need for rescues, awesome stuff.
You mentioned above that you enjoy operation chaining as in Elixir - dry-monads
can help in this quite nicely, something like:
def find_or_create(params)
Try { some_repo.find(params[:id]) } # raise an exception since the record doesn't exist
.fmap(&:itself)
.fmap { |record| <process_record> }
.or { some_repo.create(params.except(:id)) }
end
@@ -0,0 +1,5 @@ | |||
inherit_from: '../.rubocop.yml' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. What do you think are the pros/cons in using multiple rubocop
config files over a single one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actual answer to your question.
In my experience you need at least two rubocop.yml files.
One for your app
and lib
folder - to determine the style for your code.
Another one for your spec
folder - to determine the style for your tests.
Tests usually have different style requirements than regular code, especially
when using RSpec's DSL.
Having more than two config files leads to inconsistencies, and is a bad practice in my opinion.
Ramblings about rubocop-rspec
I'm pretty sure rubocop-rspec
automatically does this - it loads it's own rubocop config for the specs.
I didn't use rubocop-rspec
because it is too strict for my taste.
I've used it on previous professional projects, but spent a lot of time fighting it.
I'm sure you can learn to benefit from it's warnings, but it distracts me from what I am trying to
test, and how I want to logically structure my tests. And the fact that I have to worry about my setup
functions beeing under a specific line length is mostly a hindrance, and not a help to improve my specs.
Legacy code woes
One reason to have extra rubocop config files is when working with legacy code.
Basically you could namespace your legacy code into a Legacy
namespace, and apply
a different, far more lenient rubocop config, while your new code uses a better rubocop config.
Trying to refactor legacy code, because Rubocop complains, is a terrible idea in my opinion, as
you loose valuable context from the previous git commit, as you overwrite lines.
You can still dig through your git history, to see what the context of the previous commit to the one
that refactored for Rubocop's sake is, but that is alot of extra hassle.
spec/coffee_place/geo_spec.rb
Outdated
expect(geo.within_latitude_bounds?(0)).to eq(true) | ||
expect(geo.within_latitude_bounds?(42.0)).to eq(true) | ||
expect(geo.within_latitude_bounds?(-42.0)).to eq(true) | ||
|
||
expect(geo.within_latitude_bounds?(90.0)).to eq(true) | ||
expect(geo.within_latitude_bounds?(-90.0)).to eq(true) | ||
|
||
expect(geo.within_latitude_bounds?(92.0)).to eq(false) | ||
expect(geo.within_latitude_bounds?(-92.0)).to eq(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, why did you choose to break the rule of having one expectation per test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm lazy sometimes
Having all of these assertions in the same example here is pure laziness (tm) on my part.
I'll try to add a few more commits, describing the edge cases I am testing a bit better.
Confessions of a non-believer
That being said, I don't believe in the one expectation per test rule.
I think a test should describe and test the expectations of a single specific scenario.
I would rather have 7 expectations in the same example, testing a single well described scenario,
than break those 7 expectations out into different examples, all handling a single path through the code.
Basically, I use as many expect
calls as I find necessary to thoroughly test, but try my best
to only test a single code path in one example.
If the example fails, I now know which code path to look at.
If there are 7 different examples, with one expectation each, that break because of this
one code path, it's much harder to read and understand the rspec output.
A better tomorrow, perhaps
This being said, I know I have significant overlap in the specs I wrote for this app, and that
if a code path changes many of my specs will break.
Ideally I would like to write better, less redundant specs in the future.
no tests <
few tests <
lots of poorly thought out tests <<<
toughtful tests with useful failure output
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: fixed by 033654a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, apologies for the delayed reviewed. Second of all, very nice solution 💯 - personally I really liked the sassy style in which the description & comments were made (including the flat earth stuff lol), and the haversine function thing was nice to find out. I like the out-of-the-box approach here. 😄
I feel like I learned a thing or two from reviewing this, so my thanks on that. Left a few comments as well. :)
So Long, and Thanks for All the Fish Thank you all for your time and helpful input. ❤️ Sadly, I've already accepted a job offer this week to work for a different company. In the future, maybe a year or two, I would love to apply again for a job with your company, and work with you all. Apparently I'm now a JS front-end developer, ye-yey?! 🤔 My new job place is within 20 minutes walking distance from where I live, which I found convenient. """ The folks working over there seem nice though. I'll get to learn more about JavaScript. And more about centering and aligning things with CSS. Hoping for a better future for us all As previously stated, in the future, maybe a year or two, I would love to re-apply for a job with your company, I also really like Elixir/Phoenix as a tech stack. Rust and Elm are pretty neat too. I'd be interested in writing interactive web apps in Elixir, especially using Phoenix. |
b3887fc
to
c406cad
Compare
This separates building a location from validating it. It also makes validation logic more intuitive and similar to ActiveRecord.
When an URL to an incorrect host is used, such as "http://example.asd", the remote file download raises an uncaught SocketError. This will turn that pesky error into a nice error message.
@madalindaniel92 Aww, shucks. Well, I'm glad you found youself a nice-looking job, though I can't say I'm entirely enthusiastic since, seeing how fun it was to interact around this PR, I would have been very much delighted to work together as colleagues 😄. Still, Hopes Die Last, right?! Hope we'll have the chance to work together in year's time then. All the best, bud! 🚀 |
This implements the coding challenge.
This project uses Ruby 3.0.
Setup steps:
- clone repo
- make sure you have Ruby 3.0 installed - using
rvm
orchruby
will switch Ruby to the right version-
gem install bundler
# Make sure bundler is installed for Ruby 3.0-
bundle install
# Install dependencies-
bundle exec rspec
# Run tests-
bundle exec rubocop
# Delight in code style guide nagging :PHow to run the program:
A few assumptions are made:
In order to appease the Non-Flat-Earth believers, we could use a gem for the haversine function.
In a real world application I would use the awesome Postgresql POSTGIS extension.
It also has the ability to use spatial indexes for distance queries.