An elegant way

Hello everybody.

I’ve written a method to solve a little problem, but I find my solution
really ugly. So, I’m trying to find ways to improve it.

The method takes one argument, an array containing a sorted list of
strings representing episodes numbers. The episodes numbers are either
a number (‘1’, ‘12’) or prefixed with a letter (‘S1’ for special 1). My
goal is to find sequences in the numbers and join them with dashes :

RAniDBTools.format_episodes_list([ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ])
=> “1-4, 6-7, 9, S1-S2”

RAniDBTools.format_episodes_list([ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ])
=> “1-2, S3-S5, O6”

Here’s the code ; what can I do to improve this ?

module RAniDBTools
def RAniDBTools.format_episodes_list(list)
lt = []
le = []
list.each do |epno|
if (‘0’…‘9’).include? epno[0,1]
t = ‘’
e = epno.to_i
else
t = epno[0…0]
e = epno[1…-1].to_i
end
if lt.last == t
max = le.last.max rescue le.last
min = le.last.min rescue le.last
if e == max + 1
le[-1] = (min…e)
else
le << e
lt << t
end
else
le << e
lt << t
end
end
f = []
le.each_with_index do |e, i|
if e.is_a? Range
f << “#{lt[i]}#{e.min}-#{lt[i]}#{e.max}”
else
f << “#{lt[i]}#{e}”
end
end
f.join(’, ')
end
end

TIA,

Fred

On Mon, Sep 20, 2010 at 7:10 PM, F. Senault [email protected] wrote:

RAniDBTools.format_episodes_list([ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ])
list.each do |epno|
if e == max + 1
f = []
le.each_with_index do |e, i|
if e.is_a? Range
f << “#{lt[i]}#{e.min}-#{lt[i]}#{e.max}”
else
f << “#{lt[i]}#{e}”
end
end
f.join(', ')
end
end

I have no time to propose a complete solution. I’ll leave to you
adding the “Sx” and “Ox” processing. This is how I’d do it it they
were all numbers:

irb(main):004:0> list = [1,2,3,4,6,7,9,16,17]
=> [1, 2, 3, 4, 6, 7, 9, 16, 17]
irb(main):018:0> result = [[list.first]]
=> [[1]]
irb(main):023:0> list.each_cons(2) do |x,y|
irb(main):024:1* if (x+1) == y
irb(main):025:2> result.last << y
irb(main):026:2> else
irb(main):027:2* result << [y]
irb(main):028:2> end
irb(main):029:1> end
=> nil
irb(main):030:0> result
=> [[1, 2, 3, 4], [6, 7], [9], [16, 17]]
irb(main):031:0> s = “”
=> “”
irb(main):034:0> res = result.map {|x| x.size == 1 ? x.to_s :
“#{x.first}-#{x.last}”}
=> [“1-4”, “6-7”, “9”, “16-17”]
irb(main):035:0> res.join(“,”)
=> “1-4,6-7,9,16-17”

For the other stuff, I would have a flag that tells me if I’m in the
middle of a number, “S” or “O” run, and act accordingly.

Hope this helps,

Jesus.

On Sep 20, 2010, at 12:10 PM, F. Senault wrote:

The method takes one argument, an array containing a sorted list of
strings representing episodes numbers. The episodes numbers are either
a number (‘1’, ‘12’) or prefixed with a letter (‘S1’ for special 1). My
goal is to find sequences in the numbers and join them with dashes :

RAniDBTools.format_episodes_list([ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ])
=> “1-4, 6-7, 9, S1-S2”
RAniDBTools.format_episodes_list([ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ])
=> “1-2, S3-S5, O6”

Dave T. and I were playing around with an idea like this recently:

gist:570434 · GitHub

Trying to remove some noise, but I don't think I helped. · GitHub

I hope those give you some fresh ideas.

James Edward G. II

On Mon, Sep 20, 2010 at 1:10 PM, F. Senault [email protected] wrote:

Here’s the code ; what can I do to improve this ?
I wouldn’t call this an improvement:

def format_episodes_list a
z = []
z << a.inject([“”]){|i,j| i.last.succ==j ? i << j : (z << i ; [j])}
z[1…-1].map{|b| b.size==1?b:“#{b.first}-#{b.last}”}.join(", ")
end

puts format_episodes_list([ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’,
‘S2’ ])
#=> 1-4, 6-7, 9, S1-S2
puts format_episodes_list([ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ])
#=> 1-2, S3-S5, O6
puts format_episodes_list([ ‘1’, ‘2’, ‘9’, ‘10’, ‘S9’, ‘S10’ ])
#=> 1-2, 9-10, S9, S10

(And, I’m guessing that the last one there isn’t correct :slight_smile:

On 09/20/2010 10:10 AM, F. Senault wrote:

Hello everybody.

I’ve written a method to solve a little problem, but I find my solution
really ugly. So, I’m trying to find ways to improve it.

The method takes one argument, an array containing a sorted list of
strings representing episodes numbers. The episodes numbers are either
a number (‘1’, ‘12’) or prefixed with a letter (‘S1’ for special 1). My
goal is to find sequences in the numbers and join them with dashes :

a = [ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ]

r0 = nil
r1 = nil
result = []

a.each_cons 2 do |prev, s|
case s
when prev.succ
r0 ||= prev
r1 = s
else
if r0
result << “#{r0}-#{r1}”
else
result << prev
end
r0 = r1 = nil
end
end

if r0
result << “#{r0}-#{r1}”
else
result << a.last
end

p result # ==> [“1-2”, “S3-S5”, “O6”]

Rushed and probably full of horribleness, but it’s something, I suppose:

http://gist.github.com/588341

On 09/20/2010 11:06 AM, Joel VanderWerf wrote:

Refactoring that:

a = [ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’, “10”, “11”, “12” ]
#a = [ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ]

module Enumerable
def each_run(cond)
run = nil
last = nil

 each_cons 2 do |prev, s|
   if cond[prev, s]
     run ||= [prev]
     run << s
   else
     yield run || prev
     run = nil
   end
   last = s
 end

 yield run || [last]

 self

end

end

result = []

a.each_run(proc {|prev, s| prev.succ == s}) do |run|
if run.size > 1
result << “#{run.first}-#{run.last}”
else
result << run.first
end
end

p result

Le 20 septembre à 20:17, Joel VanderWerf a écrit :

a.each_run(proc {|prev, s| prev.succ == s}) do |run|

There’s a catch with succ :

“S9”.succ
=> “T0”

Definitely not what I want ! :slight_smile:

Fred

Le 20 septembre à 19:38, Jesús Gabriel y Galán a écrit :

/…/

I have no time to propose a complete solution. I’ll leave to you
adding the “Sx” and “Ox” processing. This is how I’d do it it they
were all numbers:

irb(main):023:0> list.each_cons(2) do |x,y|

Ahha ! each_cons…

irb(main):024:1* if (x+1) == y
irb(main):025:2> result.last << y
irb(main):026:2> else
irb(main):027:2* result << [y]
irb(main):028:2> end
irb(main):029:1> end

M’kay. My current iteration looks like this :

def RAniDBTools.format_episodes_list(list)
list.group_by { |e| (‘A’…‘Z’) === e[0,1] ? e[0,1] : ‘’ }.collect do
|p, l|
l = l.map { |e| e.gsub(/^[A-Z]/, ‘’).to_i }
format_consecutive_numbers(p, l)
end.join(", ")
end

(I’d definitely need a group_and_map function here.)

def RAniDBTools.format_consecutive_numbers(prefix, list)
result = [[ list.first ]]
list.each_cons(2) do |x, y|
if y == x + 1
result.last << y
else
result << [ y ]
end
ends
result.map { |x| “#{prefix}#{x.first}” + (x.size == 1 ? “” :
“-#{prefix}#{x.last}”) }.join(", ")
end

Better ! I’m still reading the other answers in the thread, though…

Thanks a lot for the ideas !
s
Fred

On Sep 20, 2010, at 1:45 PM, F. Senault wrote:

Where does the slice_before method comes from ?

RUBY_VERSION
=> “1.9.2”

Enumerable.public_instance_methods.include? :slice_before
=> true

It was added in 1.9.2, I believe.

James Edward G. II

Le 20 septembre à 19:44, James Edward G. II a écrit :

Dave T. and I were playing around with an idea like this recently:

gist:570434 · GitHub

Trying to remove some noise, but I don't think I helped. · GitHub

I hope those give you some fresh ideas.

Where does the slice_before method comes from ?

(Anyways, seeing your comment on the first bit of code, I think I should
use inject in my refactored form… probably…)

Fred

Le 20 septembre à 20:59, James Edward G. II a écrit :

RUBY_VERSION
=> “1.9.2”

Enumerable.public_instance_methods.include? :slice_before
=> true

It was added in 1.9.2, I believe.

RUBY_VERSION
=> “1.9.1”

Enumerable.public_instance_methods.include? :slice_before
=> false

Indeed. Explains why I can’t find it in my third edition Pick Axe…

Fred

On 09/20/2010 11:40 AM, F. Senault wrote:

Fred

Ow! Yup. Maybe you could use this:

“S9”.sub(/\d+$/) {|digits| digits.succ}
=> “S10”

On Tue, Sep 21, 2010 at 2:10 AM, F. Senault [email protected] wrote:

Hello everybody.

My
goal is to find sequences in the numbers and join them with dashes :

This is not a complete solution but maybe it is something worth
looking at.
It depends on your specs.
I’ll leave it to you to work out the details.

arr = [ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ]
s = (‘1’…‘20’).to_a + (‘S1’…‘S5’).to_a

t = []
s.each do |x|
t << x if arr.include?(x)
t << “*” if arr.include?(x) == false
end

p t.join(" “).split(”*“).map{|y| y.strip.split(” ")}.select{|z| z.size

0}.map{|w| “#{w[0]}-#{w[-1]}”}

#> [“1-4”, “6-7”, “9-9”, “S1-S2”]

Harry

On Tue, Sep 21, 2010 at 2:17 PM, Harry K. [email protected]
wrote:

0}.map{|w| “#{w[0]}-#{w[-1]}”}

#> [“1-4”, “6-7”, “9-9”, “S1-S2”]

r = [ ‘1’, ‘2’, ‘3’,‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ]
s = (‘1’…‘20’).to_a + (‘S1’…‘S5’).to_a

p [].tap{|m| s.each{|x| m << (r.include?(x) ? x : ““)}}.join(”
“).split(”
”).map{|y| y.strip.split(" ")}.select{|z| z.size >
0}.map{|w| (w.size>1 ? “#{w[0]}-#{w[-1]}” : “#{w[0]}”)}

#> [“1-4”, “6-7”, “9”, “S1-S2”]

I’ll shut up, now.

Harry

On Tue, Sep 21, 2010 at 12:51 PM, Harry K. [email protected]
wrote:

p t.join(" “).split(”*“).map{|y| y.strip.split(” ")}.select{|z| z.size

0}.map{|w| “#{w[0]}-#{w[-1]}”}

#> [“1-4”, “6-7”, “9-9”, “S1-S2”]

A slightly shorter but still unreadable version :slight_smile:

r = [ ‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’ ]
s = (‘1’…‘20’).to_a + (‘S1’…‘S5’).to_a

p [].tap{|m| s.each {|x| m << (r.include?(x) ? x : ““)}}.join(”
“).split(”
”).map{|y| y.strip.split(" ")}.select{|z| z.size >
0}.map{|w| “#{w[0]}-#{w[-1]}”}

#> [“1-4”, “6-7”, “9-9”, “S1-S2”]

Harry

In fact, I forgot it’s OK to run off the end of an Array in ruby :slight_smile: So
the inner loop can simplify to this:

j += 1 while src[j+1] == src[j].sub(/\d+/) { $&.succ }

Here is a very traditional imperative solution.

def format_episodes_list(src)
res = []
j = 0
while j < src.size
i = j
j += 1 while j+1 < src.size &&
src[j+1] == src[j].sub(/\d+/) { $&.succ }
res << ((i == j) ? src[i] : “#{src[i]}-#{src[j]}”)
j += 1
end
res.join(", ")
end

puts format_episodes_list([‘1’, ‘2’, ‘3’, ‘4’, ‘6’, ‘7’, ‘9’, ‘S1’, ‘S2’
])
puts format_episodes_list([ ‘1’, ‘2’, ‘S3’, ‘S4’, ‘S5’, ‘O6’ ])

On Tue, Sep 21, 2010 at 11:00 AM, Robert K.
[email protected] wrote:

def succ
def Num(s)
%r{\A(\D*)(\d+)\z} =~ s or raise “bad arg %p” % s
Num.new $1, $2.to_i
end

Then we can set up a generic mechanism for this

Forgot the end handling and mixed up two versions. Sorry for that.
Here’s the proper one

module Enumerable
def rangify(&bl)
bl ||= lambda {|x, y| x == y ? x : (x…y)}

res = []
a = b = nil

each do |x|
  case
  when a.nil?
    a = b = x
  when x == b.succ
    b = x
  else
    res << bl[a, b]
    a = b = x
  end
end

res << bl[a, b]

end
end

Full story is here Solution suggestion for http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/370127 · GitHub

Cheers

robert

On Tue, Sep 21, 2010 at 10:38 AM, Brian C. [email protected]
wrote:

Fred, for the contrast here’s my engineering approach. It may not be
elegant (you decide) but you get modularity and reusability.

First I’d start out with a proper representation. If your numbers are
a central part of your application you may want to spend a class for
them:

Num = Struct.new :pre, :val do
def succ
self.class.new pre, val.succ
end

def to_s; “#{pre}#{val}” end
def inspect; “#{self.class.name}(#{self})” end

include Comparable
def <=>(o) to_a <=> o.to_a end
end

def Num(s)
%r{\A(\D*)(\d+)\z} =~ s or raise “bad arg %p” % s
Num.new $1, $2.to_i
end

Then we can set up a generic mechanism for this

module Enumerable
def rangify(&bl)
bl ||= lambda {|x, y| y ? (x…y) : x}

res = []
a = b = nil

each do |x|
  case
  when a.nil?
    a = b = x
  when x == b.succ
    b = x
  else
    res << bl[a, b]
    a = b = x
  end
end

res

end
end

Full story is here Solution suggestion for http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/370127 · GitHub

Kind regards

robert