Today I Learned

A Hashrocket project

4 posts by koriroys @koriroys

Ruby 2.7 Enumerable#tally

Have you ever wanted to get back a count of how often objects showed up within an array? If so, you’ve likely written (or copy-pasta’d) some variation of the code below:

list = %w(red red red blue blue fish blue blue)

list.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }          # => {"red"=>3, "blue"=>4, "fish"=>1}
list.each_with_object({}) { |v, h| h[v] ? h[v] += 1 : h[v] = 1 } # => {"red"=>3, "blue"=>4, "fish"=>1}
list.group_by { |v| v }.map { |k, v| [k, v.size] }.to_h          # => {"red"=>3, "blue"=>4, "fish"=>1}

Lucky for us, Ruby 2.7 introduces a new convenience method in Enumerable#tally, so our above code reduces to:

list = %w(red red red blue blue fish blue blue)
list.tally # => {"red"=>3, "blue"=>4, "fish"=>1}

Huzzah!

You can read the feature discussion here!

Ruby Array#sort_by

Let’s say we have the following list of very interesting items.

items = [
  { name: "dogfood", price: 4.99 },
  { name: "catfood", price: 5.99 },
  { name: "batfood", price: 4.99 }
]

Our client wants the list sorted by price descending, and we should break ties in price by then sorting by item name descending. So with our example list, the result should be:

items = [
  { name: "batfood", price: 4.99 },
  { name: "dogfood", price: 4.99 },
  { name: "catfood", price: 5.99 }
]

Here’s a method that does the job for our client:

def sort_by_price_and_name(items)
  items
    .group_by { |i| i[:price] }
    .map { |_k,v| v.sort { |a, b| a[:name] <=> b[:name] } }
    .flatten
end

It’s a bit unwieldy and inelegant. It turns out there is a perfect method for this in Ruby:

def sort_by_price_and_name(items)
  items.sort_by { |i| [i[:price], i[:name]] }
end

Ruby’s Enumerable#sort_by works in this case where we sort by all the fields in ascending order. If we wanted to sort by price ascending, then name descending, sort_by would no longer be the tool we’d want to use.

Hat tip to @joshcheek for the knowledge.

Useful Git Diff Filenames

If you type git config --global diff.noprefix true into your terminal, it will set the value like so in your ~/.gitconfig file:

[diff]
    noprefix = true

The upshot is that git diffs will no longer prefix files with a/ and b/, such that you can copy a useful path when you double-click to select it in the terminal.

--- a/file/that/changed.awesome
+++ b/file/that/changed.awesome

becomes

--- file/that/changed.awesome
+++ file/that/changed.awesome

Credit to @brandur for the tip. Here’s the source tweet

Migrating to form_with

Whilst upgrading a Rails application from 4.1 to 5.2, my feature specs started throwing a strange error:

Unable to find field "X" that is disabled (Capybara::ElementNotFound)

I reverted my changes… the tests passed. Step 1 in debugging: read the error message. Capybara is saying the element I’m trying to find is disabled. If I inspect the html generated by form_for:

= form_for book do |f|
  = f.label :title, 'Title'
  = f.text_field :title
  = f.button 'Save Changes', type: :submit
<form action="/books/14" accept-charset="UTF-8" method="post">
  <label for="book_title">Title</label>
  <input type="text" name="book[title]" id="book_title">
  <button name="button" type="submit">Save Changes</button>
</form>

and compare it to the html generated by form_with:

= form_with model: book, local: true do |f|
  = f.label :title, 'Title'
  = f.text_field :title
  = f.button 'Save Changes', type: :submit
<form action="/books/14" accept-charset="UTF-8" method="post">
  <label for="book_title">Title</label>
  <input type="text" name="book[title]">
  <button name="button" type="submit">Save Changes</button>
</form>

Notice what’s missing. form_with does not automatically generates ids for form elements, and the id is necessary to link the label’s for with the input’s id, otherwise fill_in('Title', with: "Pride and Prejudice") doesn’t work in Capybara. Either add the ids manually, or in Rails 5.2 use this setting:

Rails.application.config.action_view.form_with_generates_ids = true