# An elegant way...

Discussion in 'Ruby' started by F. Senault, Sep 20, 2010.

1. ### F. SenaultGuest

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}#{e.min}-#{lt}#{e.max}"
else
f << "#{lt}#{e}"
end
end
f.join(', ')
end
end

TIA,

Fred
--
This is the right time, once in a lifetime So I find it hard to sleep,
don't you know The sun is shining in my window, life's in flow Making
music in the morning, laughter's light Creativity touches in for flight
(The Corrs, The Right Time)

F. Senault, Sep 20, 2010

2. ### Jesús Gabriel y GalánGuest

On Mon, Sep 20, 2010 at 7:10 PM, F. Senault <> wrote:
> Hello everybody.
>
> I've written a method to solve a little problem, but I find my solution
> really ugly. =A0So, 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. =A0The episodes numbers are either
> a number ('1', '12') or prefixed with a letter ('S1' for special 1). =A0M=

y
> 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' ])
> =3D> "1-4, 6-7, 9, S1-S2"
>>> RAniDBTools.format_episodes_list([ '1', '2', 'S3', 'S4', 'S5', 'O6' ])

> =3D> "1-2, S3-S5, O6"
>
> Here's the code ; what can I do to improve this ?
>
> module RAniDBTools
> =A0def RAniDBTools.format_episodes_list(list)
> =A0 =A0lt =3D []
> =A0 =A0le =3D []
> =A0 =A0list.each do |epno|
> =A0 =A0 =A0if ('0'..'9').include? epno[0,1]
> =A0 =A0 =A0 =A0t =3D ''
> =A0 =A0 =A0 =A0e =3D epno.to_i
> =A0 =A0 =A0else
> =A0 =A0 =A0 =A0t =3D epno[0..0]
> =A0 =A0 =A0 =A0e =3D epno[1..-1].to_i
> =A0 =A0 =A0end
> =A0 =A0 =A0if lt.last =3D=3D t
> =A0 =A0 =A0 =A0max =3D le.last.max rescue le.last
> =A0 =A0 =A0 =A0min =3D le.last.min rescue le.last
> =A0 =A0 =A0 =A0if e =3D=3D max + 1
> =A0 =A0 =A0 =A0 =A0le[-1] =3D (min..e)
> =A0 =A0 =A0 =A0else
> =A0 =A0 =A0 =A0 =A0le << e
> =A0 =A0 =A0 =A0 =A0lt << t
> =A0 =A0 =A0 =A0end
> =A0 =A0 =A0else
> =A0 =A0 =A0 =A0le << e
> =A0 =A0 =A0 =A0lt << t
> =A0 =A0 =A0end
> =A0 =A0end
> =A0 =A0f =3D []
> =A0 =A0le.each_with_index do |e, i|
> =A0 =A0 =A0if e.is_a? Range
> =A0 =A0 =A0 =A0f << "#{lt}#{e.min}-#{lt}#{e.max}"
> =A0 =A0 =A0else
> =A0 =A0 =A0 =A0f << "#{lt}#{e}"
> =A0 =A0 =A0end
> =A0 =A0end
> =A0 =A0f.join(', ')
> =A0end
> 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 =3D [1,2,3,4,6,7,9,16,17]
=3D> [1, 2, 3, 4, 6, 7, 9, 16, 17]
irb(main):018:0> result =3D [[list.first]]
=3D> [[1]]
irb(main):023:0> list.each_cons(2) do |x,y|
irb(main):024:1* if (x+1) =3D=3D 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
=3D> nil
irb(main):030:0> result
=3D> [[1, 2, 3, 4], [6, 7], [9], [16, 17]]
irb(main):031:0> s =3D ""
=3D> ""
irb(main):034:0> res =3D result.map {|x| x.size =3D=3D 1 ? x.to_s :
"#{x.first}-#{x.last}"}
=3D> ["1-4", "6-7", "9", "16-17"]
irb(main):035:0> res.join(",")
=3D> "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.

Jesús Gabriel y Galán, Sep 20, 2010

3. ### James Edward Gray IIGuest

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 :
>=20
>>> RAniDBTools.format_episodes_list([ '1', '2', '3', '4', '6', '7', =

'9', 'S1', 'S2' ])
> =3D> "1-4, 6-7, 9, S1-S2"
>>> RAniDBTools.format_episodes_list([ '1', '2', 'S3', 'S4', 'S5', 'O6' =

])
> =3D> "1-2, S3-S5, O6"

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

http://gist.github.com/570434

http://gist.github.com/570556

I hope those give you some fresh ideas.

James Edward Gray II=

James Edward Gray II, Sep 20, 2010
4. ### Guest

On Mon, Sep 20, 2010 at 1:10 PM, F. Senault <> wrote:
> The method takes one argument, an array containing a sorted list of
> strings representing episodes numbers. =A0The episodes numbers are either
> a number ('1', '12') or prefixed with a letter ('S1' for special 1). =A0M=

y
> 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' ])
> =3D> "1-4, 6-7, 9, S1-S2"
>>> RAniDBTools.format_episodes_list([ '1', '2', 'S3', 'S4', 'S5', 'O6' ])

> =3D> "1-2, S3-S5, O6"
>
> Here's the code ; what can I do to improve this ?

I wouldn't call this an improvement:

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

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

(And, I'm guessing that the last one there isn't correct

, Sep 20, 2010
5. ### Joel VanderWerfGuest

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"]

Joel VanderWerf, Sep 20, 2010
6. ### Adam PrescottGuest

Adam Prescott, Sep 20, 2010
7. ### Joel VanderWerfGuest

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

Joel VanderWerf, Sep 20, 2010
8. ### F. SenaultGuest

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
--
If I don't think about the fact that she left me,
If I don't see the pearls fall from the sky,
If I don't hear the accusations of blasphemy,
If I don't feel the tears in my eyes,
This is the best day of my life. (Prince, 3 chains o' gold)

F. Senault, Sep 20, 2010
9. ### F. SenaultGuest

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 !

Fred
--
You ask me if I've known love
And what it's like to sing songs in the rain Well, I've seen love come
And I've seen it shot down I've seen it die in vain
Shot down in a blaze of glory (Bon Jovi, Blaze of Glory)

F. Senault, Sep 20, 2010
10. ### F. SenaultGuest

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

> Dave Thomas and I were playing around with an idea like this recently:
>
> http://gist.github.com/570434
>
> http://gist.github.com/570556
>
> 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
--
When I'm nice, people think I'm up to something and start sending me
bombs in the mail.
(Mike, CRFH - http://www.crfh.net/)

F. Senault, Sep 20, 2010
11. ### Joel VanderWerfGuest

On 09/20/2010 11:40 AM, F. Senault wrote:
> 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 !
>
> Fred

Ow! Yup. Maybe you could use this:

>> "S9".sub(/\d+\$/) {|digits| digits.succ}

=> "S10"

Joel VanderWerf, Sep 20, 2010
12. ### James Edward Gray IIGuest

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

> Le 20 septembre =E0 19:44, James Edward Gray II a =E9crit :
>=20
>> Dave Thomas and I were playing around with an idea like this =

recently:
>>=20
>> http://gist.github.com/570434
>>=20
>> http://gist.github.com/570556
>>=20
>> I hope those give you some fresh ideas.

>=20
> Where does the slice_before method comes from ?

>> RUBY_VERSION

=3D> "1.9.2"
>> Enumerable.public_instance_methods.include? :slice_before

=3D> true

It was added in 1.9.2, I believe.

James Edward Gray II=

James Edward Gray II, Sep 20, 2010
13. ### F. SenaultGuest

Le 20 septembre à 20:59, James Edward Gray 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
--
The problem with defending the purity of the English language is that
English is about as pure as a cribhouse whore. We don't just borrow
words; on occasion, English has pursued other languages down alleyways
to beat them unconscious and rifle their pockets for new vocabulary.
(James D. Nicoll, rasfw)

F. Senault, Sep 20, 2010
14. ### Harry KakuekiGuest

On Tue, Sep 21, 2010 at 2:10 AM, F. Senault <> wrote:
> Hello everybody.
>
> =A0My
> 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 a=
t.
It depends on your specs.
I'll leave it to you to work out the details.

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

t =3D []
s.each do |x|
t << x if arr.include?(x)
t << "*" if arr.include?(x) =3D=3D 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

Harry Kakueki, Sep 21, 2010
15. ### Harry KakuekiGuest

On Tue, Sep 21, 2010 at 12:51 PM, Harry Kakueki <> wrote=
:
>
> arr =3D [ '1', '2', '3', '4', '6', '7', '9', 'S1', 'S2' ]
> s =3D ('1'..'20').to_a + ('S1'..'S5').to_a
>
> t =3D []
> s.each do |x|
> =A0t << x if arr.include?(x)
> =A0t << "*" if arr.include?(x) =3D=3D 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"]
>
>

A slightly shorter but still unreadable version

r =3D [ '1', '2', '3', '4', '6', '7', '9', 'S1', 'S2' ]
s =3D ('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

Harry Kakueki, Sep 21, 2010
16. ### Harry KakuekiGuest

On Tue, Sep 21, 2010 at 2:17 PM, Harry Kakueki <> wrote:
> On Tue, Sep 21, 2010 at 12:51 PM, Harry Kakueki <> wrote:
>>

>
> A slightly shorter but still unreadable version
>
> 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"]
>

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

Harry Kakueki, Sep 21, 2010
17. ### Brian CandlerGuest

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 : "#{src}-#{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' ])
--
Posted via http://www.ruby-forum.com/.

Brian Candler, Sep 21, 2010
18. ### Brian CandlerGuest

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

j += 1 while src[j+1] == src[j].sub(/\d+/) { \$&.succ }
--
Posted via http://www.ruby-forum.com/.

Brian Candler, Sep 21, 2010
19. ### Robert KlemmeGuest

On Tue, Sep 21, 2010 at 10:38 AM, Brian Candler <> 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 re, :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 http://gist.github.com/589427

Kind regards

robert

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Robert Klemme, Sep 21, 2010
20. ### Robert KlemmeGuest

On Tue, Sep 21, 2010 at 11:00 AM, Robert Klemme
<> wrote:
> On Tue, Sep 21, 2010 at 10:38 AM, Brian Candler <> wro=

te:
>
> Fred, for the contrast here's my engineering approach. =A0It may not be
> elegant (you decide) but you get modularity and reusability.
>
> First I'd start out with a proper representation. =A0If your numbers are
> a central part of your application you may want to spend a class for
> them:
>
> Num =3D Struct.new re, :val do
> =A0def succ
> =A0 =A0self.class.new pre, val.succ
> =A0end
>
> =A0def to_s; "#{pre}#{val}" end
> =A0def inspect; "#{self.class.name}(#{self})" end
>
> =A0include Comparable
> =A0def <=3D>(o) to_a <=3D> o.to_a end
> end
>
> def Num(s)
> =A0%r{\A(\D*)(\d+)\z} =3D~ s or raise "bad arg %p" % s
> =A0Num.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 ||=3D lambda {|x, y| x =3D=3D y ? x : (x..y)}

res =3D []
a =3D b =3D nil

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

res << bl[a, b]
end
end

> Full story is here http://gist.github.com/589427

Cheers

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Robert Klemme, Sep 21, 2010