Class: Censive
- Inherits:
-
StringScanner
- Object
- StringScanner
- Censive
- Defined in:
- lib/censive.rb
Class Method Summary collapse
Instance Method Summary collapse
- #<<(row) ⇒ Object
- #bomb(msg) ⇒ Object
- #each ⇒ Object
-
#grok(str) ⇒ Object
grok returns: 2 for seps and quotes, 1 for seps only, and 0 for neither.
-
#initialize(str = nil, sep: ',', quote: '"', drop: false, eol: "\n", excel: false, mode: :compact, out: nil, relax: false, **opts) ⇒ Censive
constructor
A new instance of Censive.
-
#next_char ⇒ Object
==[ Lexer ]==.
- #next_row ⇒ Object
- #next_token ⇒ Object
-
#parse ⇒ Object
==[ Parser ]==.
- #reset(str = nil) ⇒ Object
- #stats ⇒ Object
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 |