Class: Censive

Inherits:
StringScanner
  • Object
show all
Defined in:
lib/censive.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(str = nil, sep: ',', quote: '"', out: nil, mode: :compact) ⇒ Censive

Returns a new instance of Censive.



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/censive.rb', line 36

def initialize(str=nil, sep: ',', quote: '"', out: nil, mode: :compact)
  super(str || '')
  reset
  @sep   = sep  .freeze
  @quote = quote.freeze
  @es    = ""   .freeze
  @cr    = "\r" .freeze
  @lf    = "\n" .freeze
  @out   = out
  @esc   = (@quote * 2).freeze
  @mode  = mode
end

Class Method Details

.writer(path, **opts) ⇒ Object



30
31
32
33
34
# File 'lib/censive.rb', line 30

def self.writer(path, **opts)
  File.open(path, 'w') do |file|
    yield new(out: file, **opts)
  end
end

Instance Method Details

#<<(row) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/censive.rb', line 136

def <<(row)
  @out or return super

  # most compact export format
  s,q = @sep, @quote
  out = case @mode
  when :compact
    case grok(row.join)
    when 0 then row
    when 1 then row.map {|col| col.include?(s) ? "#{q}#{col}#{q}" : col }
    else
      row.map do |col|
        case grok(col)
        when 0 then col
        when 1 then "#{q}#{col}#{q}"
        else        "#{q}#{col.gsub(q, @esc)}#{q}"
        end
      end
    end
  when :full
    row.map {|col| "#{q}#{col.gsub(q, @esc)}#{q}" }
  end.join(s)

  #!# TODO: allow an option to remove trailing seps in the output
  # out.gsub!(/#{s}+\z/,'')

  #!# TODO: allow these line endings to be configurable
  @out << out + @lf
end

#bomb(msg) ⇒ Object



101
102
103
# File 'lib/censive.rb', line 101

def bomb(msg)
  abort "#{File.basename($0)}: #{msg} at character #{pos} near '#{string[pos-4,7]}'"
end

#each ⇒ Object



166
167
168
169
# File 'lib/censive.rb', line 166

def each
  @rows ||= parse
  @rows.each {|row| yield row }
end

#grok(str) ⇒ Object

grok returns 2 (seps and quotes), 1 (seps only), 0 (neither)



128
129
130
131
132
133
134
# File 'lib/censive.rb', line 128

def grok(str)
  if pos = str.index(/(#{@quote})|#{@sep}/o)
    $1 ? 2 : str.index(/#{@quote}/o, pos) ? 2 : 1
  else
    0
  end
end

#next_char ⇒ Object

==[ Lexer ]==



61
62
63
64
# File 'lib/censive.rb', line 61

def next_char
  getch
  @char = string[pos]
end

#next_row ⇒ Object



118
119
120
121
122
123
# File 'lib/censive.rb', line 118

def next_row
  token = next_token or return
  row = [token]
  row << token while token = next_token
  row
end

#next_token ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/censive.rb', line 66

def next_token
  case @flag
  when @es then @flag = nil; [@cr,@lf,nil].include?(@char) and return @es
  when @cr then @flag = nil; next_char == @lf and next_char
  when @lf then @flag = nil; next_char
  end if @flag

  if [@sep,@quote,@cr,@lf,nil].include?(@char)
    case @char
    when @quote # consume_quoted_cell
      match = ""
      while true
        getch # consume the quote (optimized by not calling next_char)
        match << (scan_until(/(?=#{@quote})/o) or bomb "unclosed quote")
        case next_char
        when @sep        then @flag = @es; next_char; break
        when @quote      then match << @quote
        when @cr,@lf,nil then break
        else bomb "unexpected character after quote"
        end
      end
      match
    when @sep then @flag = @es; next_char; @es
    when @cr  then @flag = @cr; nil
    when @lf  then @flag = @lf; nil
    when nil  then nil
    end
  else # consume_unquoted_cell
    match = scan_until(/(?=#{@sep}|#{@cr}|#{@lf}|\z)/o) or bomb "unexpected character"
    @char = string[pos]
    @char == @sep and @flag = @es and next_char
    match
  end
end

#parse ⇒ Object

==[ Parser ]==



107
108
109
110
111
112
113
114
115
116
# File 'lib/censive.rb', line 107

def parse
  @rows ||= []
  while row = next_row
    @rows << row
    size = row.size
    @cols = size if size > @cols
    @cells += size
  end
  @rows
end

#reset(str = nil) ⇒ Object



49
50
51
52
53
54
55
56
57
# File 'lib/censive.rb', line 49

def reset(str=nil)
  self.string = str if str
  super()
  @char  = string[pos]
  @flag  = nil

  @rows  = nil
  @cols  = @cells = 0
end

#stats ⇒ Object



171
172
173
174
175
176
177
# File 'lib/censive.rb', line 171

def stats
  wide = string.size.to_s.size
  puts "%#{wide}d rows"    % @rows.size
  puts "%#{wide}d columns" % @cols
  puts "%#{wide}d cells"   % @cells
  puts "%#{wide}d bytes"   % string.size
end