[Ruby] Get 2-4, 7, 9 from [4, 7, 9, 2, 3]

6 minute read

Introduction

This article was written by looking at the following articles.
I tried to solve a simple programming problem using Ruby pattern matching –Qiita

The above article

"1, 5, 10-12, 15, 18-20"

From the character string

[1, 5, 10, 11, 12, 15, 18, 19, 20]

It describes how to get the array in Ruby.

On the other hand, this article is the opposite. In other words, given an array of positive integers, sort them and then create a comma-separated string by connecting the serial numbers with hyphens.

These processes are used, for example, to create the page number notation for a book index.

In addition, in such a notation,

  • Combine with 4-5 even if there are only two serial numbers such as 4, 5
  • If only two continue, separate them as 4,5

There are two possible styles, but in this article we will take the former (also write about the latter).

code

I also wrote test code using test-unit.

require "test/unit"

def gather(array)
  array.uniq.sort
    .chunk_while{ |a, b| a.succ == b }
    .map{ |c| (c.size == 1) ? c : "#{c[0]}-#{c[-1]}" }
    .join(", ")
end

#Below is the test code
#If you execute the script, the test described below will be performed automatically.

class TestGather < Test::Unit::TestCase
  test "Dismembered" do
    assert_equal "1, 3, 5", gather([3, 5, 1])
  end

  test "There is duplication" do
    assert_equal "2, 4", gather([4, 2, 2, 4, 4])
  end

  test "Summary" do
    assert_equal "1-2", gather([2, 1])
    assert_equal "1-3", gather([2, 3, 1])
    assert_equal "2-4, 7, 9, 11-12",
      gather([12, 11, 7, 9, 2, 4, 3])
  end
end

Commentary

Sort without duplication

At first

array.uniq.sort

As a result, duplicates are omitted and sorted. I don’t think it needs much explanation.
However, considering efficiency, it should be noted that it is better to ʻuniq first and then sort` [^ uniq-sort].

Catalyze the serial number

Next, the heart of this code is

chunk_while{ |a, b| a.succ == b }

Part of.
In fact, the official reference Enumerable # chunk_while has a sample that looks exactly like the code in this article. Although it has been done.

chunk_while is a really interesting Ruby-like method. Who is chunk_while?

This is a decision ** for an Enumerable object such as an array, whether to lick </ruby> the elements from the edge and connect or disconnect between two adjacent elements. It is a method that decides according to the criteria ** and creates a chunk in that way. However, the return value is Enumerator, not an array of catamari. I won’t explain what an Enumerator is in this article, but since it is an Enumerable object, if you to_a, it will be an array of catamari.

For example

[2, 6, 0, 3, 5, 8]

Let’s say that there is an array called, and we want to divide this into even-numbered and odd-numbered categories.
In other words

[[2, 6, 0], [3, 5], [8]]

I want the array.
The important thing is that only ** adjacent ** even numbers and odd numbers ** are ** catalyzed.
Therefore, the last 8 is not combined with 2, 6, 0, but is an excursion.

By the way, how should we write the conditional expression that the two integers ʻa and b` are both even numbers or both odd numbers?
One way is

(a.even? && b.even?) || (a.odd? && b.odd?)

But you don’t have to do this kind of trouble.
If you notice that “the difference between even and even is even”, “the difference between odd and odd is even”, and “the difference between even and odd is odd”

(a - b).even?

I know it’s okay.

Now, to get an array of adjacent even and odd numbers, write as follows using chunk_while.

numbers = [2, 6, 0, 3, 5, 8]

p numbers.chunk_while{ |a, b| (a - b).even? }.to_a
# => [[2, 6, 0], [3, 5], [8]]

How does this chunk_while work?
chunk_while uses ʻeach to retrieve each element from the receiver. First of all, do so to extract the two elements. In this case, 2 and 6 are retrieved. Pass these two to the block and evaluate the block. Then (2 -6) .even? Is true. At this time, chunk_while decides," Hmmmm, then I won't disconnect between 2 and 6. " Next, pass the already retrieved 6 and the next retrieved 0 to the block. Again, the block returns true, so it does not disconnect. Similarly, pass 0 and 3 to the block, which returns false. Then, chunk_while` decides,” Okay! Cut it off here! “.

The same applies below.

The way chunk_while picks up two adjacent elements while shifting their positions one by one is similar to ʻeach_cons (2). ʻEach_cons (2) just picks up two elements, while chunk_while picks up two elements to determine where to join / disconnect.

Now that you know how chunk_while works,

chunk_while{ |a, b| a.succ == b }

Let’s consider.
ʻInteger # succ is a method that returns an integer obtained by adding 1 to itself. ʻA.succ == b expresses the condition that “after ʻa is b` “.

Hyphens the serial numbers

Next

map{ |c| (c.size == 1) ? c : "#{c[0]}-#{c[-1]}" }

Is easy.

Oops, a word before that. Since the return value of chunk_while is an Enumerator object, it is not necessary to make an array with to_a, and map can be continued as it is.

By the way, regarding this map block, if each catamari is 1 in length, it is left as it is, and if it is 2 or more, the beginning and end are connected with a hyphen.

For example, [7] is left as [7], and [2, 3, 4] is changed to " 2-4 ".
Well, the converted array is a bit unpleasant because the elements are arrays and strings. No, there is no problem with this (described later), but it is certain that it will be a little mushy.

Connect with commas

Finally

join(", ")

Finish with.
Well, this is just connecting the elements with ", ", but to be confident that I said in the previous paragraph that “elements can be arrays or strings, it doesn’t matter”, Array # join needs an accurate understanding.

join returns a string that is connected with an argument in between if all the elements are strings.
If some of the elements aren’t strings, they’re first stringed before they’re connected, but if it’s an array, they’re stringed using join instead of to_s. At that time, join uses the same arguments as the original join.

So

p [1, [2, 3]].join("-") # => "1-2-3"

become that way.

With the above, the operation is completely understood.

variation

As I foretold in the “Introduction” section, how can I modify it so that it is not hyphenated when there are only two consecutive numbers such as 4,5?

In other words

p gather([1, 2, 4, 5, 6]) # => "1, 2, 4-6"

How to make it

Actually, the sample code of Enumerable # chunk_while is written according to this style.

To do so

.map{ |c| (c.size == 1) ? c : "#{c[0]}-#{c[-1]}" }

To

.map{ |c| (c.size < 3) ? c : "#{c[0]}-#{c[-1]}" }

You can change it to.
It is clear from the specifications of join already mentioned that this modification is sufficient.

Digression

It’s unfortunate that Ruby’s similar unit test library remains split into test-unit and minitest.

In my eyes, test-unit seems to be better [^ tu], but Rails has adopted minitest (a magical modification of it?), So minitest may be superior in the world. unknown.

e? RSpec? No, I don’t feel like I can write it because it’s too unclear to me. What is ʻit`?

Tags:

Updated: