LoginSignup
2
3

More than 5 years have passed since last update.

連続値をまとめる

Last updated at Posted at 2014-11-22

あるコマンド foo で、こんな出力がある

$ ./foo
"EB80"
"EB81"
"EB82"
"EB83"
"EB84"
"EB85"
"EB87"
"EB88"
"EB89"
"EB8A"
"EB8B"
"EB8D"

Unicodeの羅列なわけだけど、これが連続しているかどうかを確認したい。
普通に、Unixのコマンドで頑張ってみると

$ ./foo | tr -d '"' | (echo ibase=16; cat) | bc | awk 'last+1 == $0{last=$0; print "..."; next} {print last; last=$0; print $0} END {print last}' | uniq

60288
...
60293
60295
...
60299
60301

なんとかここまで来たが、この10進数を16進数に変換する処理がいまいちである。bcでやってみると、"..."の出力のせいでエラーになる(まあ、目的は達成している)

$ ./foo   | tr -d '"' | (echo ibase=16; cat) | bc | awk 'last+1 == $0{last=$0; print "..."; next} {print last; last=$0; print $0} END {print last}
' | uniq | (echo ibase=10; echo obase=16; cat) | bc

EB80
(standard_in) 5: syntax error
EB85
EB87
(standard_in) 8: syntax error
EB8B
EB8D

ruby で頑張ってみることにする。

まずは、連続値をうまく扱うクラスがあった方がいろいろとうれしいんじゃないかと思った。
(結構、大きくなったので保存のためにこの記事を書くことにした)

discrete_range.rb
#!/usr/bin/env ruby

class DiscreteRange
  include Enumerable

  def initialize(*arg)
    @ranges = []
    arg.each {|v| add v}
  end

  def add(v)
    entry = @ranges[-1]
    case entry
    when Range
      if !entry.exclude_end? && entry.last.succ == v
        @ranges[-1] = Range.new(entry.first, v)
      elsif entry.exclude_end? && entry.last == v
        @ranges[-1] = Range.new(entry.first, v)
      else
        @ranges.push v
      end
    else
      if entry.respond_to?(:succ) && entry.succ == v
        @ranges[-1] = Range.new(entry, v)
      else
        @ranges.push v
      end
    end
  end

  def each
    @ranges.each {|e|
      case e
      when Range
        e.each {|v|
          yield v
        }
      else
        yield e
      end
    }
  end

  def to_s
    @ranges.to_s
  end
end

if $0 == __FILE__
  dr = DiscreteRange.new(1,2,3,5)
  p dr      # => #<DiscreteRange:0x0000060042f5f8 @ranges=[1..3, 5]>
  p dr.to_a # => [1, 2, 3, 5]
  dr.add 6
  p dr      # => #<DiscreteRange:0x0000060042f238 @ranges=[1..3, 5..6]>
end
./foo | tr -d '"' | ruby -r./discrete_range.rb -e 'args = ARGF.readlines.map(&:chomp)' -e 'dr = DiscreteRange.new(*args)' -e 'puts dr'
["EB80".."EB85", "EB87".."EB89", "EB8A".."EB8B", "EB8D"]

たまたまいいサンプルを選んだが、EB89, EB8A は16進数で連続しているのにそう見なされていない。やっぱり10進数⇔16進数を相互変換するコマンドは欲しい。

とりあえずrubyで代用する

$ ./foo | tr -d '"' | ruby -ne 'puts $_.hex' | ruby -r./discrete_range.rb -e 'args = ARGF.readlines.map(&:chomp)' -e 'dr = DiscreteRange.new(*args)' -e 'puts dr' | ruby -pe '$_.gsub!(/\d+/) { $&.to_i.to_s(16).upcase }'
["EB80".."EB85", "EB87".."EB8B", "EB8D"]

もちろんパイプを使わずに全部一つのrubyでやってもいいが、Unix的哲学に反する気がするので、役割分担の意味でコマンドを分けている。

一応、rubyでやってみたがやはりワンライナーでは適材適所があると思う。

$ ./foo | ruby -r./discrete_range.rb -e 'args = ARGF.readlines.map {|v| v.delete("\"")}.map(&:hex)' -e 'dr = DiscreteRange.new(*args)' -e 'puts dr.to_s.gsub(/\d+/) { $&.to_i.to_s(16).upcase }'
[EB80..EB85, EB87..EB8B, EB8D]
2
3
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3