{"id":13878365,"url":"https://github.com/testdouble/put","last_synced_at":"2025-07-16T14:31:57.329Z","repository":{"id":59862710,"uuid":"539610085","full_name":"testdouble/put","owner":"testdouble","description":null,"archived":false,"fork":false,"pushed_at":"2022-10-04T15:18:47.000Z","size":56,"stargazers_count":96,"open_issues_count":1,"forks_count":1,"subscribers_count":6,"default_branch":"main","last_synced_at":"2024-10-31T14:27:25.874Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/testdouble.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-09-21T17:34:24.000Z","updated_at":"2024-09-04T12:37:08.000Z","dependencies_parsed_at":"2022-09-23T20:41:36.115Z","dependency_job_id":null,"html_url":"https://github.com/testdouble/put","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fput","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fput/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fput/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fput/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/testdouble","download_url":"https://codeload.github.com/testdouble/put/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226138849,"owners_count":17579496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-06T08:01:47.455Z","updated_at":"2024-11-24T07:30:58.532Z","avatar_url":"https://github.com/testdouble.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Put puts your objects in order 💎\n\nPut pairs with\n[Enumerable#sort_by](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-sort_by)\nto provide a more expressive, fault-tolerant, and configurable approach to\nsorting Ruby objects with multiple criteria. [Here's a screencast \u0026 short blog \npost](https://blog.testdouble.com/talks/2022-09-28-a-better-way-to-sort-ruby-objects/)\nthat we put out, in case you're interested.\n\n## Put \"put\" in your Gemfile\n\nYou've probably already put a few gems in there, so why not put Put, too:\n\n```ruby\ngem \"put\"\n```\n\nOf course after you push Put, your colleagues will wonder why you put Put there.\n\n## Before you tell me where to put it\n\nA neat trick when applying complex sorting rules to a collection is to map them\nto an array of arrays of comparable values in priority order. It's a common\napproach (and a special subtype of what's called a [Schwartzian\ntransform](https://en.wikipedia.org/wiki/Schwartzian_transform)), but the\npattern doesn't have a widely-accepted name yet, so let's use code to explain.\n\nSuppose you have some people:\n\n```ruby\nPerson = Struct.new(:name, :age, :rubyist?, keyword_init: true)\n\npeople = [\n  Person.new(name: \"Tam\", age: 22),\n  Person.new(name: \"Zak\", age: 33),\n  Person.new(name: \"Axe\", age: 33),\n  Person.new(name: \"Qin\", age: 18, rubyist?: true),\n  Person.new(name: \"Zoe\", age: 28, rubyist?: true)\n]\n```\n\nAnd you want to sort these people in the following priority order:\n\n1. Put any Rubyists at the _top_ of the list, as is right and good\n2. If both are (or are not) Rubyists, break the tie by sorting by age descending\n3. Finally, break any remaining ties by sorting by name ascending\n\nHere's what the aforementioned pattern to accomplish this usually looks like\nusing\n[Enumerable#sort_by](https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-sort_by):\n\n```ruby\npeople.sort_by { |person|\n  [\n    person.rubyist? ? 0 : 1,\n    person.age * -1,\n    person.name\n  ]\n} # =\u003e Zoe, Qin, Axe, Zak, Tam\n```\n\nThe above will return everyone in the right order. This has a few drawbacks,\nthough:\n\n* Unless you're already familiar with this pattern that nobody's bothered to\ngive a name before, this code isn't very expressive. As a result, each line\nis almost begging for a code comment above it to explain its intent\n* Ternary operators are confusing, especially with predicate methods like\n`rubyist?` and especially when returning [magic\nnumber](https://en.wikipedia.org/wiki/Magic_number_(programming))'s like `1` and\n`0`.\n* Any `nil` values will result in a bad time. If a person's `age` is nil, you'll\nget \"_undefined method '*' for nil:NilClass_\" `NoMethodError`\n* Relatedly, if any two items aren't comparable (i.e. `\u003c=\u003e` returns nil), you'll\n  be greeted with an inscrutable `ArgumentError` that just says \"_comparison of\n  Array with Array failed_\"\n\nHere's the same code example if you put Put in there:\n\n```ruby\npeople.sort_by { |person|\n  [\n    (Put.first if person.rubyist?),\n    Put.desc(person.age),\n    Put.asc(person.name)\n  ]\n} # =\u003e Zoe, Qin, Axe, Zak, Tam\n```\n\nThe Put gem solves every one of the above issues:\n\n* Put's methods have actual names. In fact, let's just call this the \"Put\n  pattern\" while we're at it\n* No ternaries necessary\n* It's quite `nil` friendly\n* It ships with a `Put.debug` method that helps you introspect those\n  impenetrable `ArgumentError` messages whenever any two values turn out not to\n  be comparable\n\nAfter reading this, your teammates will be glad they put you in charge of\nputting gems like Put in the Gemfile.\n\n## When you Put it that way\n\nPut's API is short and sweet. In fact, you've already put up with most of it.\n\n### Put.first\n\nWhen a particular condition indicates an item should go to the top of a list,\nyou'll want to designate a position in your mapped `sort_by` arrays to return\neither `Put.first` or `nil`, like this:\n\n```ruby\n[42, 12, 65, 99, 49].sort_by { |n|\n  [(Put.first if n.odd?)]\n} # =\u003e 65, 99, 49, 42, 12\n```\n\n### Put.last\n\nWhen a sort criteria should go to the bottom of the list, you can do the same\nsort of conditional expression with `Put.last`:\n\n```ruby\n%w[Jin drinks Gin on Gym day].sort_by { |s|\n  [(Put.last unless s.match?(/[A-Z]/))]\n} # =\u003e [\"Jin\", \"Gin\", \"Gym\", \"drinks\", \"on\", \"day\"]\n```\n\n### Put.asc(value, nils_first: false)\n\nThe `Put.asc` method provides a nil-safe way to sort a value in ascending order:\n\n```ruby\n%w[The quick brown fox].sort_by { |s|\n  [Put.asc(s)]\n} # =\u003e [\"The\", \"brown\", \"fox\", \"quick\"]\n```\n\nIt also supports an optional `nils_first` keyword argument that defaults to\nfalse (translation: nils are sorted last by default), which looks like this:\n\n```ruby\n[3, nil, 1, 5].sort_by { |n|\n  [Put.asc(n, nils_first: true)]\n} # =\u003e [nil, 1, 3, 5]\n```\n\n### Put.desc(value, nils_first: false)\n\nThe opposite of `Put.asc` is `Put.desc`, and it works as you might suspect:\n\n```ruby\n%w[Aardvark Zebra].sort_by { |s|\n  [Put.desc(s)]\n} # =\u003e [\"Zebra\", \"Aardvark\"]\n```\n\nAnd also like `Put.asc`, `Put.desc` has an optional `nils_first` keyword\nargument when you want nils on top:\n\n```ruby\n[1, nil, 2, 3].sort_by { |n|\n  [Put.desc(n, nils_first: true)]\n} # =\u003e [nil, 3, 2, 1]\n```\n\n### Put.anywhere\n\nYou're sorting stuff, so naturally _order matters_. But when building a compound\n`sort_by` expression, order matters less as you add more and more tiebreaking\ncriteria. In fact, sometimes shuffling items is the more appropriate than\nleaving things in their original order. Enter `Put.anywhere`, which can be\ncalled without any argument at any index in the mapped sorting array:\n\n```ruby\n[1, 3, 4, 7, 8, 9].sort_by { |n|\n  [\n    (Put.first if n.even?),\n    Put.anywhere\n  ]\n} # =\u003e [8, 4, 1, 7, 9, 3]\n```\n\n### Put.nils_first(value)\n\nIf you're sorting items and you know some not-comparable `nil` values are going\nto appear, you can put all the nils on top with `Put.nil_first(value)`. Note\nthat _unlike_ `Put.asc` and `Put.desc`, it won't actually sort the values—it'll\njust pull all the nils up!\n\n```ruby\n[:fun, :stuff, nil, :here].sort_by { |val|\n  [Put.nils_first(val)]\n} # =\u003e [nil, :fun, :stuff, :here]\n```\n\n### Put.nils_last(value)\n\nAs you might be able to guess, `Put.nils_last` puts the nils last:\n\n```ruby\n[:every, nil, :counts].sort_by { |val|\n  [Put.nils_last(val)]\n} # =\u003e [:every, :counts, nil]\n```\n\n### Put.debug(sorting_arrays)\n\nIf you see \"comparison of Array with Array failed\" and you don't have any idea\nwhat is going on, try debugging by changing `sort_by` to `map` and passing it\nto `Put.debug`.\n\nFor an interactive example of how to debug this issue with `Put.debug`, take a\nlook [at this test case](/test/put_test.rb#L53-L98).\n\n## Put your hands together! 👏\n\nMany thanks to [Matt Jones](https://github.com/al2o3cr) and [Matthew\nDraper](https://github.com/matthewd) for answering a bunch of obscure questions\nabout comparisons in Ruby and implementing the initial prototype, respectively.\n👏👏👏\n\n## Code of Conduct\n\nThis project follows Test Double's [code of\nconduct](https://testdouble.com/code-of-conduct) for all community interactions,\nincluding (but not limited to) one-on-one communications, public posts/comments,\ncode reviews, pull requests, and GitHub issues. If violations occur, Test Double\nwill take any action they deem appropriate for the infraction, up to and\nincluding blocking a user from the organization's repositories.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestdouble%2Fput","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftestdouble%2Fput","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestdouble%2Fput/lists"}