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: '"', drop: false, eol: "\n", excel: false, mode: :compact, out: nil, relax: false, **opts) ⇒ Censive

Returns a new instance of Censive.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/censive.rb', line 35

def initialize(str=nil,
  sep:   ','     , # column separator character
  quote: '"'     , # quote character

  drop:  false   , # enable to drop trailing separators
  eol:   "\n"    , # desired line endings for exports
  excel: false   , # allow ,="0123" style columns
  mode:  :compact, # export mode: compact or full
  out:   nil     , # output IO/file
  relax: false   , # relax parsing of quotes

  **opts           # grab bag
)
  super(str || '')
  reset

  @sep    = sep  .freeze
  @quote  = quote.freeze

  @drop   = drop
  @eol    = eol.freeze
  @mode   = mode
  @out    = out
  @relax  = relax

  @es     = ""   .freeze
  @cr     = "\r" .freeze
  @lf     = "\n" .freeze
  @eq     = "="  .freeze
  @esc    = (@quote * 2).freeze

  @tokens = [@sep,@quote,@cr,@lf,@es,nil]
  @tokens << @eq if excel # See http://bit.ly/3Y7jIvc
end

Class Method Details

.writer(path, **opts) ⇒ Object



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

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

Instance Method Details

#<<(row) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/censive.rb', line 163

def <<(row)
  @out or return super

  # drop trailing seps, if specified
  row.pop while row.last.empty? if @drop

  # 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)

  # write output, using desired line endings
  @out << out + @eol
end

#bomb(msg) ⇒ Object



128
129
130
# File 'lib/censive.rb', line 128

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

#each ⇒ Object



193
194
195
196
# File 'lib/censive.rb', line 193

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

#grok(str) ⇒ Object

grok returns: 2 for seps and quotes, 1 for seps only, and 0 for neither



155
156
157
158
159
160
161
# File 'lib/censive.rb', line 155

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



82
83
84
85
# File 'lib/censive.rb', line 82

def next_char
  getch
  @char = peek(1)
end

#next_row ⇒ Object



145
146
147
148
149
150
# File 'lib/censive.rb', line 145

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

#next_token ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/censive.rb', line 87

def next_token
  case @flag
  when @es then @flag = nil; [@cr,@lf,@es,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 @tokens.include?(@char)
    case @char
    when @quote, @eq # consume quoted cell
      @char == @eq and next_char # excel mode: allows ,="012",
      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,@es,nil then break
        else
          if @relax
            match << @quote + @char
          else
            bomb "invalid character after quote"
          end
        end
      end
      match
    when @sep    then @flag = @es; next_char; @es
    when @cr     then @flag = @cr; nil
    when @lf     then @flag = @lf; nil
    when @es,nil then              nil
    end
  else # consume unquoted cell
    match = scan_until(/(?=#{@sep}|#{@cr}|#{@lf}|\z)/o) or bomb "unexpected character"
    @char = peek(1)
    @char == @sep and @flag = @es and next_char
    match
  end
end

#parse ⇒ Object

==[ Parser ]==



134
135
136
137
138
139
140
141
142
143
# File 'lib/censive.rb', line 134

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



70
71
72
73
74
75
76
77
78
# File 'lib/censive.rb', line 70

def reset(str=nil)
  self.string = str if str
  super()
  @char = peek(1)
  @flag = nil

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

#stats ⇒ Object



198
199
200
201
202
203
204
# File 'lib/censive.rb', line 198

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