| 1 | # csv.tcl -- |
|---|
| 2 | # |
|---|
| 3 | # Tcl implementations of CSV reader and writer |
|---|
| 4 | # |
|---|
| 5 | # Copyright (c) 2001 by Jeffrey Hobbs |
|---|
| 6 | # Copyright (c) 2001-2005 by Andreas Kupries <andreas_kupries@users.sourceforge.net> |
|---|
| 7 | # |
|---|
| 8 | # See the file "license.terms" for information on usage and redistribution |
|---|
| 9 | # of this file, and for a DISCLAIMER OF ALL WARRANTIES. |
|---|
| 10 | # |
|---|
| 11 | # RCS: @(#) $Id: csv.tcl,v 1.25 2006/09/19 23:36:15 andreas_kupries Exp $ |
|---|
| 12 | |
|---|
| 13 | package require Tcl 8.3 |
|---|
| 14 | package provide csv 0.7 |
|---|
| 15 | |
|---|
| 16 | namespace eval ::csv { |
|---|
| 17 | namespace export join joinlist read2matrix read2queue report |
|---|
| 18 | namespace export split split2matrix split2queue writematrix writequeue |
|---|
| 19 | } |
|---|
| 20 | |
|---|
| 21 | # ::csv::join -- |
|---|
| 22 | # |
|---|
| 23 | # Takes a list of values and generates a string in CSV format. |
|---|
| 24 | # |
|---|
| 25 | # Arguments: |
|---|
| 26 | # values A list of the values to join |
|---|
| 27 | # sepChar The separator character, defaults to comma |
|---|
| 28 | # |
|---|
| 29 | # Results: |
|---|
| 30 | # A string containing the values in CSV format. |
|---|
| 31 | |
|---|
| 32 | proc ::csv::join {values {sepChar ,} {delChar \"}} { |
|---|
| 33 | set out "" |
|---|
| 34 | set sep {} |
|---|
| 35 | foreach val $values { |
|---|
| 36 | if {[string match "*\[${delChar}$sepChar\]*" $val]} { |
|---|
| 37 | append out $sep${delChar}[string map [list $delChar ${delChar}${delChar}] $val]${delChar} |
|---|
| 38 | } else { |
|---|
| 39 | append out $sep${val} |
|---|
| 40 | } |
|---|
| 41 | set sep $sepChar |
|---|
| 42 | } |
|---|
| 43 | return $out |
|---|
| 44 | } |
|---|
| 45 | |
|---|
| 46 | # ::csv::joinlist -- |
|---|
| 47 | # |
|---|
| 48 | # Takes a list of lists of values and generates a string in CSV |
|---|
| 49 | # format. Each item in the list is made into a single CSV |
|---|
| 50 | # formatted record in the final string, the records being |
|---|
| 51 | # separated by newlines. |
|---|
| 52 | # |
|---|
| 53 | # Arguments: |
|---|
| 54 | # values A list of the lists of the values to join |
|---|
| 55 | # sepChar The separator character, defaults to comma |
|---|
| 56 | # |
|---|
| 57 | # Results: |
|---|
| 58 | # A string containing the values in CSV format, the records |
|---|
| 59 | # separated by newlines. |
|---|
| 60 | |
|---|
| 61 | proc ::csv::joinlist {values {sepChar ,} {delChar \"}} { |
|---|
| 62 | set out "" |
|---|
| 63 | foreach record $values { |
|---|
| 64 | # note that this is ::csv::join |
|---|
| 65 | append out "[join $record $sepChar $delChar]\n" |
|---|
| 66 | } |
|---|
| 67 | return $out |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | # ::csv::joinmatrix -- |
|---|
| 71 | # |
|---|
| 72 | # Takes a matrix object following the API specified for the |
|---|
| 73 | # struct::matrix package. Each row of the matrix is converted |
|---|
| 74 | # into a single CSV formatted record in the final string, the |
|---|
| 75 | # records being separated by newlines. |
|---|
| 76 | # |
|---|
| 77 | # Arguments: |
|---|
| 78 | # matrix Matrix object command. |
|---|
| 79 | # sepChar The separator character, defaults to comma |
|---|
| 80 | # |
|---|
| 81 | # Results: |
|---|
| 82 | # A string containing the values in CSV format, the records |
|---|
| 83 | # separated by newlines. |
|---|
| 84 | |
|---|
| 85 | proc ::csv::joinmatrix {matrix {sepChar ,} {delChar \"}} { |
|---|
| 86 | return [joinlist [$matrix get rect 0 0 end end] $sepChar $delChar] |
|---|
| 87 | } |
|---|
| 88 | |
|---|
| 89 | # ::csv::iscomplete -- |
|---|
| 90 | # |
|---|
| 91 | # A predicate checking if the argument is a complete csv record. |
|---|
| 92 | # |
|---|
| 93 | # Arguments |
|---|
| 94 | # data The (partial) csv record to check. |
|---|
| 95 | # |
|---|
| 96 | # Results: |
|---|
| 97 | # A boolean flag indicating the completeness of the data. The |
|---|
| 98 | # result is true if the data is complete. |
|---|
| 99 | |
|---|
| 100 | proc ::csv::iscomplete {data} { |
|---|
| 101 | expr {1 - [regexp -all \" $data] % 2} |
|---|
| 102 | } |
|---|
| 103 | |
|---|
| 104 | # ::csv::read2matrix -- |
|---|
| 105 | # |
|---|
| 106 | # A wrapper around "Split2matrix" reading CSV formatted |
|---|
| 107 | # lines from the specified channel and adding it to the given |
|---|
| 108 | # matrix. |
|---|
| 109 | # |
|---|
| 110 | # Arguments: |
|---|
| 111 | # m The matrix to add the read data too. |
|---|
| 112 | # chan The channel to read from. |
|---|
| 113 | # sepChar The separator character, defaults to comma |
|---|
| 114 | # expand The expansion mode. The default is none |
|---|
| 115 | # |
|---|
| 116 | # Results: |
|---|
| 117 | # A list of the values in 'line'. |
|---|
| 118 | |
|---|
| 119 | proc ::csv::read2matrix {args} { |
|---|
| 120 | # FR #481023 |
|---|
| 121 | # See 'split2matrix' for the available expansion modes. |
|---|
| 122 | |
|---|
| 123 | # Argument syntax: |
|---|
| 124 | # |
|---|
| 125 | #2) chan m |
|---|
| 126 | #3) chan m sepChar |
|---|
| 127 | #3) -alternate chan m |
|---|
| 128 | #4) -alternate chan m sepChar |
|---|
| 129 | #4) chan m sepChar expand |
|---|
| 130 | #5) -alternate chan m sepChar expand |
|---|
| 131 | |
|---|
| 132 | set alternate 0 |
|---|
| 133 | set sepChar , |
|---|
| 134 | set expand none |
|---|
| 135 | |
|---|
| 136 | switch -exact -- [llength $args] { |
|---|
| 137 | 2 { |
|---|
| 138 | foreach {chan m} $args break |
|---|
| 139 | } |
|---|
| 140 | 3 { |
|---|
| 141 | foreach {a b c} $args break |
|---|
| 142 | if {[string equal $a "-alternate"]} { |
|---|
| 143 | set alternate 1 |
|---|
| 144 | set chan $b |
|---|
| 145 | set m $c |
|---|
| 146 | } else { |
|---|
| 147 | set chan $a |
|---|
| 148 | set m $b |
|---|
| 149 | set sepChar $c |
|---|
| 150 | } |
|---|
| 151 | } |
|---|
| 152 | 4 { |
|---|
| 153 | foreach {a b c d} $args break |
|---|
| 154 | if {[string equal $a "-alternate"]} { |
|---|
| 155 | set alternate 1 |
|---|
| 156 | set chan $b |
|---|
| 157 | set m $c |
|---|
| 158 | set sepChar $d |
|---|
| 159 | } else { |
|---|
| 160 | set chan $a |
|---|
| 161 | set m $b |
|---|
| 162 | set sepChar $c |
|---|
| 163 | set expand $d |
|---|
| 164 | } |
|---|
| 165 | } |
|---|
| 166 | 5 { |
|---|
| 167 | foreach {a b c d e} $args break |
|---|
| 168 | if {![string equal $a "-alternate"]} { |
|---|
| 169 | return -code error "wrong#args: Should be ?-alternate? chan m ?separator? ?expand?" |
|---|
| 170 | } |
|---|
| 171 | set alternate 1 |
|---|
| 172 | |
|---|
| 173 | set chan $b |
|---|
| 174 | set m $c |
|---|
| 175 | set sepChar $d |
|---|
| 176 | set expand $e |
|---|
| 177 | } |
|---|
| 178 | 0 - 1 - |
|---|
| 179 | default { |
|---|
| 180 | return -code error "wrong#args: Should be ?-alternate? chan m ?separator? ?expand?" |
|---|
| 181 | } |
|---|
| 182 | } |
|---|
| 183 | |
|---|
| 184 | set data "" |
|---|
| 185 | while {![eof $chan]} { |
|---|
| 186 | if {[gets $chan line] < 0} {continue} |
|---|
| 187 | |
|---|
| 188 | # Why skip empty lines? They may be in data. Except if the |
|---|
| 189 | # buffer is empty, i.e. we are between records. |
|---|
| 190 | if {$line == {} && $data == {}} {continue} |
|---|
| 191 | |
|---|
| 192 | append data $line |
|---|
| 193 | if {![iscomplete $data]} { |
|---|
| 194 | # Odd number of quotes - must have embedded newline |
|---|
| 195 | append data \n |
|---|
| 196 | continue |
|---|
| 197 | } |
|---|
| 198 | |
|---|
| 199 | Split2matrix $alternate $m $data $sepChar $expand |
|---|
| 200 | set data "" |
|---|
| 201 | } |
|---|
| 202 | return |
|---|
| 203 | } |
|---|
| 204 | |
|---|
| 205 | # ::csv::read2queue -- |
|---|
| 206 | # |
|---|
| 207 | # A wrapper around "::csv::split2queue" reading CSV formatted |
|---|
| 208 | # lines from the specified channel and adding it to the given |
|---|
| 209 | # queue. |
|---|
| 210 | # |
|---|
| 211 | # Arguments: |
|---|
| 212 | # q The queue to add the read data too. |
|---|
| 213 | # chan The channel to read from. |
|---|
| 214 | # sepChar The separator character, defaults to comma |
|---|
| 215 | # |
|---|
| 216 | # Results: |
|---|
| 217 | # A list of the values in 'line'. |
|---|
| 218 | |
|---|
| 219 | proc ::csv::read2queue {args} { |
|---|
| 220 | # Argument syntax: |
|---|
| 221 | # |
|---|
| 222 | #2) chan q |
|---|
| 223 | #3) chan q sepChar |
|---|
| 224 | #3) -alternate chan q |
|---|
| 225 | #4) -alternate chan q sepChar |
|---|
| 226 | |
|---|
| 227 | set alternate 0 |
|---|
| 228 | set sepChar , |
|---|
| 229 | |
|---|
| 230 | switch -exact -- [llength $args] { |
|---|
| 231 | 2 { |
|---|
| 232 | foreach {chan q} $args break |
|---|
| 233 | } |
|---|
| 234 | 3 { |
|---|
| 235 | foreach {a b c} $args break |
|---|
| 236 | if {[string equal $a "-alternate"]} { |
|---|
| 237 | set alternate 1 |
|---|
| 238 | set chan $b |
|---|
| 239 | set q $c |
|---|
| 240 | } else { |
|---|
| 241 | set chan $a |
|---|
| 242 | set q $b |
|---|
| 243 | set sepChar $c |
|---|
| 244 | } |
|---|
| 245 | } |
|---|
| 246 | 4 { |
|---|
| 247 | foreach {a b c d} $args break |
|---|
| 248 | if {![string equal $a "-alternate"]} { |
|---|
| 249 | return -code error "wrong#args: Should be ?-alternate? chan q ?separator?" |
|---|
| 250 | } |
|---|
| 251 | set alternate 1 |
|---|
| 252 | set chan $b |
|---|
| 253 | set q $c |
|---|
| 254 | set sepChar $d |
|---|
| 255 | } |
|---|
| 256 | 0 - 1 - |
|---|
| 257 | default { |
|---|
| 258 | return -code error "wrong#args: Should be ?-alternate? chan q ?separator?" |
|---|
| 259 | } |
|---|
| 260 | } |
|---|
| 261 | |
|---|
| 262 | set data "" |
|---|
| 263 | while {![eof $chan]} { |
|---|
| 264 | if {[gets $chan line] < 0} {continue} |
|---|
| 265 | |
|---|
| 266 | # Why skip empty lines? They may be in data. Except if the |
|---|
| 267 | # buffer is empty, i.e. we are between records. |
|---|
| 268 | if {$line == {} && $data == {}} {continue} |
|---|
| 269 | |
|---|
| 270 | append data $line |
|---|
| 271 | if {![iscomplete $data]} { |
|---|
| 272 | # Odd number of quotes - must have embedded newline |
|---|
| 273 | append data \n |
|---|
| 274 | continue |
|---|
| 275 | } |
|---|
| 276 | |
|---|
| 277 | $q put [Split $alternate $line $sepChar] |
|---|
| 278 | set data "" |
|---|
| 279 | } |
|---|
| 280 | return |
|---|
| 281 | } |
|---|
| 282 | |
|---|
| 283 | # ::csv::report -- |
|---|
| 284 | # |
|---|
| 285 | # A report command which can be used by the matrix methods |
|---|
| 286 | # "format-via" and "format2chan-via". For the latter this |
|---|
| 287 | # command delegates the work to "::csv::writematrix". "cmd" is |
|---|
| 288 | # expected to be either "printmatrix" or |
|---|
| 289 | # "printmatrix2channel". The channel argument, "chan", has to |
|---|
| 290 | # be present for the latter and must not be present for the first. |
|---|
| 291 | # |
|---|
| 292 | # Arguments: |
|---|
| 293 | # cmd Either 'printmatrix' or 'printmatrix2channel' |
|---|
| 294 | # matrix The matrix to format. |
|---|
| 295 | # args 0 (chan): The channel to write to |
|---|
| 296 | # |
|---|
| 297 | # Results: |
|---|
| 298 | # None for 'printmatrix2channel', else the CSV formatted string. |
|---|
| 299 | |
|---|
| 300 | proc ::csv::report {cmd matrix args} { |
|---|
| 301 | switch -exact -- $cmd { |
|---|
| 302 | printmatrix { |
|---|
| 303 | if {[llength $args] > 0} { |
|---|
| 304 | return -code error "wrong # args:\ |
|---|
| 305 | ::csv::report printmatrix matrix" |
|---|
| 306 | } |
|---|
| 307 | return [joinlist [$matrix get rect 0 0 end end]] |
|---|
| 308 | } |
|---|
| 309 | printmatrix2channel { |
|---|
| 310 | if {[llength $args] != 1} { |
|---|
| 311 | return -code error "wrong # args:\ |
|---|
| 312 | ::csv::report printmatrix2channel matrix chan" |
|---|
| 313 | } |
|---|
| 314 | writematrix $matrix [lindex $args 0] |
|---|
| 315 | return "" |
|---|
| 316 | } |
|---|
| 317 | default { |
|---|
| 318 | return -code error "Unknown method $cmd" |
|---|
| 319 | } |
|---|
| 320 | } |
|---|
| 321 | } |
|---|
| 322 | |
|---|
| 323 | # ::csv::split -- |
|---|
| 324 | # |
|---|
| 325 | # Split a string according to the rules for CSV processing. |
|---|
| 326 | # This assumes that the string contains a single line of CSVs |
|---|
| 327 | # |
|---|
| 328 | # Arguments: |
|---|
| 329 | # line The string to split |
|---|
| 330 | # sepChar The separator character, defaults to comma |
|---|
| 331 | # |
|---|
| 332 | # Results: |
|---|
| 333 | # A list of the values in 'line'. |
|---|
| 334 | |
|---|
| 335 | proc ::csv::split {args} { |
|---|
| 336 | # Argument syntax: |
|---|
| 337 | # |
|---|
| 338 | # (1) line |
|---|
| 339 | # (2) line sepChar |
|---|
| 340 | # (2) -alternate line |
|---|
| 341 | # (3) -alternate line sepChar |
|---|
| 342 | |
|---|
| 343 | # (3) line sepChar delChar |
|---|
| 344 | # (4) -alternate line sepChar delChar |
|---|
| 345 | |
|---|
| 346 | set alternate 0 |
|---|
| 347 | set sepChar , |
|---|
| 348 | set delChar \" |
|---|
| 349 | |
|---|
| 350 | switch -exact -- [llength $args] { |
|---|
| 351 | 1 { |
|---|
| 352 | set line [lindex $args 0] |
|---|
| 353 | } |
|---|
| 354 | 2 { |
|---|
| 355 | foreach {a b} $args break |
|---|
| 356 | if {[string equal $a "-alternate"]} { |
|---|
| 357 | set alternate 1 |
|---|
| 358 | set line $b |
|---|
| 359 | } else { |
|---|
| 360 | set line $a |
|---|
| 361 | set sepChar $b |
|---|
| 362 | } |
|---|
| 363 | } |
|---|
| 364 | 3 { |
|---|
| 365 | foreach {a b c} $args break |
|---|
| 366 | if {[string equal $a "-alternate"]} { |
|---|
| 367 | set alternate 1 |
|---|
| 368 | set line $b |
|---|
| 369 | set sepChar $c |
|---|
| 370 | } else { |
|---|
| 371 | set line $a |
|---|
| 372 | set sepChar $b |
|---|
| 373 | set delChar $c |
|---|
| 374 | } |
|---|
| 375 | } |
|---|
| 376 | 4 { |
|---|
| 377 | foreach {a b c d} $args break |
|---|
| 378 | if {![string equal $a "-alternate"]} { |
|---|
| 379 | return -code error "wrong#args: Should be ?-alternate? line ?separator? ?delimiter?" |
|---|
| 380 | } |
|---|
| 381 | set alternate 1 |
|---|
| 382 | set line $b |
|---|
| 383 | set sepChar $c |
|---|
| 384 | set delChar $d |
|---|
| 385 | } |
|---|
| 386 | 0 - |
|---|
| 387 | default { |
|---|
| 388 | return -code error "wrong#args: Should be ?-alternate? line ?separator? ?delimiter?" |
|---|
| 389 | } |
|---|
| 390 | } |
|---|
| 391 | |
|---|
| 392 | if {[string length $sepChar] < 1} { |
|---|
| 393 | return -code error "illegal separator character ${delChar}$sepChar${delChar}, is empty" |
|---|
| 394 | } elseif {[string length $sepChar] > 1} { |
|---|
| 395 | return -code error "illegal separator character ${delChar}$sepChar${delChar}, is a string" |
|---|
| 396 | } |
|---|
| 397 | |
|---|
| 398 | if {[string length $delChar] < 1} { |
|---|
| 399 | return -code error "illegal separator character \"$delChar\", is empty" |
|---|
| 400 | } elseif {[string length $delChar] > 1} { |
|---|
| 401 | return -code error "illegal separator character \"$delChar\", is a string" |
|---|
| 402 | } |
|---|
| 403 | |
|---|
| 404 | return [Split $alternate $line $sepChar $delChar] |
|---|
| 405 | } |
|---|
| 406 | |
|---|
| 407 | proc ::csv::Split {alternate line sepChar {delChar \"}} { |
|---|
| 408 | # Protect the sepchar from special interpretation by |
|---|
| 409 | # the regex calls below. |
|---|
| 410 | |
|---|
| 411 | set sepRE \\$sepChar |
|---|
| 412 | set delRE \\$delChar |
|---|
| 413 | |
|---|
| 414 | if {$alternate} { |
|---|
| 415 | # The alternate syntax requires a different parser. |
|---|
| 416 | # A variation of the string map / regsub parser for the |
|---|
| 417 | # regular syntax was tried but does not handle embedded |
|---|
| 418 | # doubled " well (testcase csv-91.3 was 'knownBug', sole |
|---|
| 419 | # one, still a bug). Now we just tokenize the input into |
|---|
| 420 | # the primary parts (sep char, "'s and the rest) and then |
|---|
| 421 | # use an explicitly coded state machine (DFA) to parse |
|---|
| 422 | # and convert token sequences. |
|---|
| 423 | |
|---|
| 424 | ## puts 1->>$line<< |
|---|
| 425 | set line [string map [list \ |
|---|
| 426 | $sepChar \0$sepChar\0 \ |
|---|
| 427 | $delChar \0${delChar}\0 \ |
|---|
| 428 | ] $line] |
|---|
| 429 | |
|---|
| 430 | ## puts 2->>$line<< |
|---|
| 431 | set line [string map [list \0\0 \0] $line] |
|---|
| 432 | regsub "^\0" $line {} line |
|---|
| 433 | regsub "\0$" $line {} line |
|---|
| 434 | |
|---|
| 435 | ## puts 3->>$line<< |
|---|
| 436 | |
|---|
| 437 | set val "" |
|---|
| 438 | set res "" |
|---|
| 439 | set state base |
|---|
| 440 | |
|---|
| 441 | ## puts 4->>[::split $line \0] |
|---|
| 442 | foreach token [::split $line \0] { |
|---|
| 443 | |
|---|
| 444 | ## puts "\t*= $state\t>>$token<<" |
|---|
| 445 | switch -exact -- $state { |
|---|
| 446 | base { |
|---|
| 447 | if {[string equal $token "${delChar}"]} { |
|---|
| 448 | set state qvalue |
|---|
| 449 | continue |
|---|
| 450 | } |
|---|
| 451 | if {[string equal $token $sepChar]} { |
|---|
| 452 | lappend res $val |
|---|
| 453 | set val "" |
|---|
| 454 | continue |
|---|
| 455 | } |
|---|
| 456 | append val $token |
|---|
| 457 | } |
|---|
| 458 | qvalue { |
|---|
| 459 | if {[string equal $token "${delChar}"]} { |
|---|
| 460 | # May end value, may be a doubled " |
|---|
| 461 | set state endordouble |
|---|
| 462 | continue |
|---|
| 463 | } |
|---|
| 464 | append val $token |
|---|
| 465 | } |
|---|
| 466 | endordouble { |
|---|
| 467 | if {[string equal $token "${delChar}"]} { |
|---|
| 468 | # Doubled ", append to current value |
|---|
| 469 | append val ${delChar} |
|---|
| 470 | set state qvalue |
|---|
| 471 | continue |
|---|
| 472 | } |
|---|
| 473 | # Last " was end of quoted value. Close it. |
|---|
| 474 | # We expect current as $sepChar |
|---|
| 475 | |
|---|
| 476 | lappend res $val |
|---|
| 477 | set val "" |
|---|
| 478 | set state base |
|---|
| 479 | |
|---|
| 480 | if {[string equal $token $sepChar]} {continue} |
|---|
| 481 | |
|---|
| 482 | # Undoubled " in middle of text. Just assume that |
|---|
| 483 | # remainder is another qvalue. |
|---|
| 484 | set state qvalue |
|---|
| 485 | } |
|---|
| 486 | default { |
|---|
| 487 | return -code error "Internal error, illegal parsing state" |
|---|
| 488 | } |
|---|
| 489 | } |
|---|
| 490 | } |
|---|
| 491 | |
|---|
| 492 | ## puts "/= $state\t>>$val<<" |
|---|
| 493 | |
|---|
| 494 | lappend res $val |
|---|
| 495 | |
|---|
| 496 | ## puts 5->>$res<< |
|---|
| 497 | return $res |
|---|
| 498 | } else { |
|---|
| 499 | regsub -- "$sepRE${delRE}${delRE}$" $line $sepChar\0${delChar}${delChar}\0 line |
|---|
| 500 | regsub -- "^${delRE}${delRE}$sepRE" $line \0${delChar}${delChar}\0$sepChar line |
|---|
| 501 | regsub -all -- {(^${delChar}|${delChar}$)} $line \0 line |
|---|
| 502 | |
|---|
| 503 | set line [string map [list \ |
|---|
| 504 | $sepChar${delChar}${delChar}${delChar} $sepChar\0${delChar} \ |
|---|
| 505 | ${delChar}${delChar}${delChar}$sepChar ${delChar}\0$sepChar \ |
|---|
| 506 | ${delChar}${delChar} ${delChar} \ |
|---|
| 507 | ${delChar} \0 \ |
|---|
| 508 | ] $line] |
|---|
| 509 | |
|---|
| 510 | set end 0 |
|---|
| 511 | while {[regexp -indices -start $end -- {(\0)[^\0]*(\0)} $line \ |
|---|
| 512 | -> start end]} { |
|---|
| 513 | set start [lindex $start 0] |
|---|
| 514 | set end [lindex $end 0] |
|---|
| 515 | set range [string range $line $start $end] |
|---|
| 516 | if {[string first $sepChar $range] >= 0} { |
|---|
| 517 | set line [string replace $line $start $end \ |
|---|
| 518 | [string map [list $sepChar \1] $range]] |
|---|
| 519 | } |
|---|
| 520 | incr end |
|---|
| 521 | } |
|---|
| 522 | set line [string map [list $sepChar \0 \1 $sepChar \0 {} ] $line] |
|---|
| 523 | return [::split $line \0] |
|---|
| 524 | |
|---|
| 525 | } |
|---|
| 526 | } |
|---|
| 527 | |
|---|
| 528 | # ::csv::split2matrix -- |
|---|
| 529 | # |
|---|
| 530 | # Split a string according to the rules for CSV processing. |
|---|
| 531 | # This assumes that the string contains a single line of CSVs. |
|---|
| 532 | # The resulting list of values is appended to the specified |
|---|
| 533 | # matrix, as a new row. The code assumes that the matrix provides |
|---|
| 534 | # the same interface as the queue provided by the 'struct' |
|---|
| 535 | # module of tcllib, "add row" in particular. |
|---|
| 536 | # |
|---|
| 537 | # Arguments: |
|---|
| 538 | # m The matrix to write the resulting list to. |
|---|
| 539 | # line The string to split |
|---|
| 540 | # sepChar The separator character, defaults to comma |
|---|
| 541 | # expand The expansion mode. The default is none |
|---|
| 542 | # |
|---|
| 543 | # Results: |
|---|
| 544 | # A list of the values in 'line', written to 'q'. |
|---|
| 545 | |
|---|
| 546 | proc ::csv::split2matrix {args} { |
|---|
| 547 | # FR #481023 |
|---|
| 548 | |
|---|
| 549 | # Argument syntax: |
|---|
| 550 | # |
|---|
| 551 | #2) m line |
|---|
| 552 | #3) m line sepChar |
|---|
| 553 | #3) -alternate m line |
|---|
| 554 | #4) -alternate m line sepChar |
|---|
| 555 | #4) m line sepChar expand |
|---|
| 556 | #5) -alternate m line sepChar expand |
|---|
| 557 | |
|---|
| 558 | set alternate 0 |
|---|
| 559 | set sepChar , |
|---|
| 560 | set expand none |
|---|
| 561 | |
|---|
| 562 | switch -exact -- [llength $args] { |
|---|
| 563 | 2 { |
|---|
| 564 | foreach {m line} $args break |
|---|
| 565 | } |
|---|
| 566 | 3 { |
|---|
| 567 | foreach {a b c} $args break |
|---|
| 568 | if {[string equal $a "-alternate"]} { |
|---|
| 569 | set alternate 1 |
|---|
| 570 | set m $b |
|---|
| 571 | set line $c |
|---|
| 572 | } else { |
|---|
| 573 | set m $a |
|---|
| 574 | set line $b |
|---|
| 575 | set sepChar $c |
|---|
| 576 | } |
|---|
| 577 | } |
|---|
| 578 | 4 { |
|---|
| 579 | foreach {a b c d} $args break |
|---|
| 580 | if {[string equal $a "-alternate"]} { |
|---|
| 581 | set alternate 1 |
|---|
| 582 | set m $b |
|---|
| 583 | set line $c |
|---|
| 584 | set sepChar $d |
|---|
| 585 | } else { |
|---|
| 586 | set m $a |
|---|
| 587 | set line $b |
|---|
| 588 | set sepChar $c |
|---|
| 589 | set expand $d |
|---|
| 590 | } |
|---|
| 591 | } |
|---|
| 592 | 4 { |
|---|
| 593 | foreach {a b c d e} $args break |
|---|
| 594 | if {![string equal $a "-alternate"]} { |
|---|
| 595 | return -code error "wrong#args: Should be ?-alternate? m line ?separator? ?expand?" |
|---|
| 596 | } |
|---|
| 597 | set alternate 1 |
|---|
| 598 | |
|---|
| 599 | set m $b |
|---|
| 600 | set line $c |
|---|
| 601 | set sepChar $d |
|---|
| 602 | set expand $e |
|---|
| 603 | } |
|---|
| 604 | 0 - 1 - |
|---|
| 605 | default { |
|---|
| 606 | return -code error "wrong#args: Should be ?-alternate? m line ?separator? ?expand?" |
|---|
| 607 | } |
|---|
| 608 | } |
|---|
| 609 | |
|---|
| 610 | Split2matrix $alternate $m $line $sepChar $expand |
|---|
| 611 | return |
|---|
| 612 | } |
|---|
| 613 | |
|---|
| 614 | proc ::csv::Split2matrix {alternate m line sepChar expand} { |
|---|
| 615 | set csv [Split $alternate $line $sepChar] |
|---|
| 616 | |
|---|
| 617 | # Expansion modes |
|---|
| 618 | # - none : default, behaviour of original implementation. |
|---|
| 619 | # no expansion is done, lines are silently truncated |
|---|
| 620 | # to the number of columns in the matrix. |
|---|
| 621 | # |
|---|
| 622 | # - empty : A matrix without columns is expanded to the number |
|---|
| 623 | # of columns in the first line added to it. All |
|---|
| 624 | # following lines are handled as if "mode == none" |
|---|
| 625 | # was set. |
|---|
| 626 | # |
|---|
| 627 | # - auto : Full auto-mode. The matrix is expanded as needed to |
|---|
| 628 | # hold all columns of all lines. |
|---|
| 629 | |
|---|
| 630 | switch -exact -- $expand { |
|---|
| 631 | none {} |
|---|
| 632 | empty { |
|---|
| 633 | if {[$m columns] == 0} { |
|---|
| 634 | $m add columns [llength $csv] |
|---|
| 635 | } |
|---|
| 636 | } |
|---|
| 637 | auto { |
|---|
| 638 | if {[$m columns] < [llength $csv]} { |
|---|
| 639 | $m add columns [expr {[llength $csv] - [$m columns]}] |
|---|
| 640 | } |
|---|
| 641 | } |
|---|
| 642 | } |
|---|
| 643 | $m add row $csv |
|---|
| 644 | return |
|---|
| 645 | } |
|---|
| 646 | |
|---|
| 647 | # ::csv::split2queue -- |
|---|
| 648 | # |
|---|
| 649 | # Split a string according to the rules for CSV processing. |
|---|
| 650 | # This assumes that the string contains a single line of CSVs. |
|---|
| 651 | # The resulting list of values is appended to the specified |
|---|
| 652 | # queue, as a single item. IOW each item in the queue represents |
|---|
| 653 | # a single CSV record. The code assumes that the queue provides |
|---|
| 654 | # the same interface as the queue provided by the 'struct' |
|---|
| 655 | # module of tcllib, "put" in particular. |
|---|
| 656 | # |
|---|
| 657 | # Arguments: |
|---|
| 658 | # q The queue to write the resulting list to. |
|---|
| 659 | # line The string to split |
|---|
| 660 | # sepChar The separator character, defaults to comma |
|---|
| 661 | # |
|---|
| 662 | # Results: |
|---|
| 663 | # A list of the values in 'line', written to 'q'. |
|---|
| 664 | |
|---|
| 665 | proc ::csv::split2queue {args} { |
|---|
| 666 | # Argument syntax: |
|---|
| 667 | # |
|---|
| 668 | #2) q line |
|---|
| 669 | #3) q line sepChar |
|---|
| 670 | #3) -alternate q line |
|---|
| 671 | #4) -alternate q line sepChar |
|---|
| 672 | |
|---|
| 673 | set alternate 0 |
|---|
| 674 | set sepChar , |
|---|
| 675 | |
|---|
| 676 | switch -exact -- [llength $args] { |
|---|
| 677 | 2 { |
|---|
| 678 | foreach {q line} $args break |
|---|
| 679 | } |
|---|
| 680 | 3 { |
|---|
| 681 | foreach {a b c} $args break |
|---|
| 682 | if {[string equal $a "-alternate"]} { |
|---|
| 683 | set alternate 1 |
|---|
| 684 | set q $b |
|---|
| 685 | set line $c |
|---|
| 686 | } else { |
|---|
| 687 | set q $a |
|---|
| 688 | set line $b |
|---|
| 689 | set sepChar $c |
|---|
| 690 | } |
|---|
| 691 | } |
|---|
| 692 | 4 { |
|---|
| 693 | foreach {a b c d} $args break |
|---|
| 694 | if {![string equal $a "-alternate"]} { |
|---|
| 695 | return -code error "wrong#args: Should be ?-alternate? q line ?separator?" |
|---|
| 696 | } |
|---|
| 697 | set alternate 1 |
|---|
| 698 | |
|---|
| 699 | set q $b |
|---|
| 700 | set line $c |
|---|
| 701 | set sepChar $d |
|---|
| 702 | } |
|---|
| 703 | 0 - 1 - |
|---|
| 704 | default { |
|---|
| 705 | return -code error "wrong#args: Should be ?-alternate? q line ?separator?" |
|---|
| 706 | } |
|---|
| 707 | } |
|---|
| 708 | |
|---|
| 709 | $q put [Split $alternate $line $sepChar] |
|---|
| 710 | return |
|---|
| 711 | } |
|---|
| 712 | |
|---|
| 713 | # ::csv::writematrix -- |
|---|
| 714 | # |
|---|
| 715 | # A wrapper around "::csv::join" taking the rows in a matrix and |
|---|
| 716 | # writing them as CSV formatted lines into the channel. |
|---|
| 717 | # |
|---|
| 718 | # Arguments: |
|---|
| 719 | # m The matrix to take the data to write from. |
|---|
| 720 | # chan The channel to write into. |
|---|
| 721 | # sepChar The separator character, defaults to comma |
|---|
| 722 | # |
|---|
| 723 | # Results: |
|---|
| 724 | # None. |
|---|
| 725 | |
|---|
| 726 | proc ::csv::writematrix {m chan {sepChar ,} {delChar \"}} { |
|---|
| 727 | set n [$m rows] |
|---|
| 728 | for {set r 0} {$r < $n} {incr r} { |
|---|
| 729 | puts $chan [join [$m get row $r] $sepChar $delChar] |
|---|
| 730 | } |
|---|
| 731 | |
|---|
| 732 | # Memory intensive alternative: |
|---|
| 733 | # puts $chan [joinlist [m get rect 0 0 end end] $sepChar $delChar] |
|---|
| 734 | return |
|---|
| 735 | } |
|---|
| 736 | |
|---|
| 737 | # ::csv::writequeue -- |
|---|
| 738 | # |
|---|
| 739 | # A wrapper around "::csv::join" taking the rows in a queue and |
|---|
| 740 | # writing them as CSV formatted lines into the channel. |
|---|
| 741 | # |
|---|
| 742 | # Arguments: |
|---|
| 743 | # q The queue to take the data to write from. |
|---|
| 744 | # chan The channel to write into. |
|---|
| 745 | # sepChar The separator character, defaults to comma |
|---|
| 746 | # |
|---|
| 747 | # Results: |
|---|
| 748 | # None. |
|---|
| 749 | |
|---|
| 750 | proc ::csv::writequeue {q chan {sepChar ,} {delChar \"}} { |
|---|
| 751 | while {[$q size] > 0} { |
|---|
| 752 | puts $chan [join [$q get] $sepChar $delChar] |
|---|
| 753 | } |
|---|
| 754 | |
|---|
| 755 | # Memory intensive alternative: |
|---|
| 756 | # puts $chan [joinlist [$q get [$q size]] $sepChar $delChar] |
|---|
| 757 | return |
|---|
| 758 | } |
|---|
| 759 | |
|---|