Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,19 @@
|
||||
## Reader example
|
||||
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
data := 'x,y\na,b,c\n'
|
||||
mut parser := csv.new_reader(data)
|
||||
// read each line
|
||||
for {
|
||||
items := parser.read() or { break }
|
||||
println(items)
|
||||
}
|
||||
```
|
||||
|
||||
It prints:
|
||||
```
|
||||
['x', 'y']
|
||||
['a', 'b', 'c']
|
||||
```
|
||||
@@ -0,0 +1,355 @@
|
||||
# CSV Reader
|
||||
There are two CSV readers in this module:
|
||||
|
||||
* Random Access reader
|
||||
* Sequential reader
|
||||
|
||||
# Sequential CSV reader
|
||||
The sequential reader read the file row by row using only the memory needed for readings.
|
||||
Here is a very simple example of usage:
|
||||
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
fn main() {
|
||||
mut csvr :=
|
||||
csv.csv_sequential_reader(file_path: 'big2.csv', end_line_len: csv.endline_crlf_len)!
|
||||
for csvr.has_data() > 1 {
|
||||
println(csvr.get_next_row()!)
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
This is the simplest way to use it to read csv files in sequential mode,
|
||||
with default configuration every cell is read as `string`.
|
||||
The function `get_row()` is used to read a single row, and it returns an array of `string`.
|
||||
|
||||
## Reading from different sources `csv_sequential_reader`
|
||||
The CSV Sequential Reader can read from files, and memory buffers.
|
||||
|
||||
### read from a file
|
||||
```v ignore
|
||||
csv.csv_sequential_reader(file_path:file_path)
|
||||
```
|
||||
### read from a memory buffer
|
||||
```v ignore
|
||||
csv.csv_sequential_reader(scr_buf:voidptr(buffer_ptr), scr_buf_len: buffer_len)
|
||||
```
|
||||
When you call `csv.csv_sequential_reader` a `SequentialReader` struct is initialized passing
|
||||
a `SequentialReaderConfig` struct as a parameter.
|
||||
Using these structs, it is possible to change the behavior of the CSV Reader.
|
||||
|
||||
## The `SequentialReaderConfig` struct
|
||||
The config struct is as follows:
|
||||
```v ignore
|
||||
pub struct SequentialReaderConfig {
|
||||
pub:
|
||||
scr_buf voidptr // pointer to the buffer of data
|
||||
scr_buf_len i64 // if > 0 use the RAM pointed by scr_buf as source of data
|
||||
file_path string
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
mem_buf_size int = 1024 * 64 // default buffer size 64KByte
|
||||
separator u8 = `,`
|
||||
comment u8 = `#` // every line that start with the comment char is ignored
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string // return this string if empty cell
|
||||
end_line_len int = endline_cr_len // size of the endline rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
}
|
||||
```
|
||||
|Field|Description|
|
||||
|------------|--------------|
|
||||
|`scr_buf`, `scr_buf_len`|If `scr_buf_len > 0` the reader will use the `scr_buf` pointer as the base address of the data to parse and `scr_buf_len` as the length of the buffer itself|
|
||||
|`file_path`| if `scr_buf_len == 0` the reader will try to open the `file_path` file|
|
||||
|`start_index`,`end_index`| **Internal usage for now**|
|
||||
|`mem_buf_size`|memory allocated for the reading operations on the file, more memory more speed|
|
||||
|`separator`|char used as cell separator in the CSV file, default is comma|
|
||||
|`comment`|every line that start with the comment char is ignored|
|
||||
|`default_cell`|return this string if the query coordinates are out of the csv boundaries|
|
||||
|`empty_cell`|return this string if the query coordinates are on an empty cell|
|
||||
|`end_line_len`|size of the endline, `endline_cr_len=1`,`endline_crlf_len=2`|
|
||||
|`quote`|quote char for the cells|
|
||||
|
||||
|
||||
# Random Access CSV Reader
|
||||
The Random Access CSV file reader indexes the file before reading the data.
|
||||
This indexing operation permits access to every cell of the CSV file in random order.
|
||||
Here is a very simple example of usage:
|
||||
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
const txt = '
|
||||
a,b,c
|
||||
0,1,2
|
||||
3,4,5
|
||||
'
|
||||
|
||||
fn main() {
|
||||
mut csvr := csv.csv_reader_from_string(txt)!
|
||||
// scan all rows, csvr.csv_map.len contain the valid
|
||||
// rows number in the CSV file.
|
||||
for row_index in 0 .. csvr.csv_map.len {
|
||||
row := csvr.get_row(row_index)!
|
||||
println(row)
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
will give the following output:
|
||||
```
|
||||
['a', 'b', 'c']
|
||||
['0', '1', '2']
|
||||
['3', '4', '5']
|
||||
```
|
||||
This is the simplest way to use it to read csv files in a random access mode,
|
||||
with default configuration every cell is read as `string`.
|
||||
The function `get_row()` is used to read a single row, and it returns an array of `string`.
|
||||
|
||||
## Reading from different sources `csv_reader`
|
||||
The CSV Random access Reader can read from files, strings, memory buffers.
|
||||
### read from a file
|
||||
```v ignore
|
||||
csv.csv_reader(file_path:file_path)
|
||||
```
|
||||
### read from a string
|
||||
```v ignore
|
||||
csv.csv_reader_from_string(string_with_the_csv)
|
||||
```
|
||||
*Note: csv_reader_from_string is "syntax sugar" for buffered reading*
|
||||
### read from a memory buffer
|
||||
```v ignore
|
||||
csv.csv_reader(scr_buf:voidptr(buffer_ptr), scr_buf_len: buffer_len)
|
||||
```
|
||||
When you call `csv.csv_reader` a `RandomAccessReader` struct is initialized passing
|
||||
a `RandomAccessReaderConfig` struct as a parameter.
|
||||
Using these structs, it is possible to change the behavior of the CSV Reader.
|
||||
|
||||
## The `RandomAccessReaderConfig` struct
|
||||
The config struct is as follows:
|
||||
```v ignore
|
||||
pub struct RandomAccessReaderConfig {
|
||||
pub:
|
||||
scr_buf voidptr // pointer to the buffer of data
|
||||
scr_buf_len i64 // if > 0 use the RAM pointed from scr_buf as source of data
|
||||
file_path string
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
mem_buf_size int = 1024 * 64 // default buffer size 64KByte
|
||||
separator u8 = `,`
|
||||
comment u8 = `#` // every line that start with the comment char is ignored
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string // return this string if empty cell
|
||||
end_line_len int = csv.endline_cr_len // size of the endline rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
quote_remove bool // if true clear the cell from the quotes
|
||||
}
|
||||
```
|
||||
|Field|Description|
|
||||
|------------|--------------|
|
||||
|`scr_buf`, `scr_buf_len`|If `scr_buf_len > 0` the reader will use the `scr_buf` pointer as the base address of the data to parse and `scr_buf_len` as the length of the buffer itself|
|
||||
|`file_path`| if `scr_buf_len == 0` the reader will try to open the `file_path` file|
|
||||
|`start_index`,`end_index`| **Internal usage for now**|
|
||||
|`mem_buf_size`|memory allocated for the reading operations on the file, more memory more speed|
|
||||
|`separator`|char used as cell separator in the CSV file, default is comma|
|
||||
|`comment`|every line that start with the comment char is ignored
|
||||
|`default_cell`|return this string if the query coordinates are out of the csv boundaries|
|
||||
|`empty_cell`|return this string if the query coordinates are on an empty cell|
|
||||
|`end_line_len`|size of the endline, `endline_cr_len=1`,`endline_crlf_len=2`|
|
||||
|`quote`|quote char for the cells|
|
||||
|`quote_remove`| if **true** try to remove the quotes from each cell, use only in quoted csv files|
|
||||
|
||||
## Random Access reading
|
||||
The main feature of this module is to allow random access to CSV file cells,
|
||||
such as this example with custom configuration:
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
const txt = '
|
||||
a,b,c
|
||||
0,1,2
|
||||
3,,5
|
||||
'
|
||||
|
||||
fn main() {
|
||||
mut csvr := csv.csv_reader_from_string(txt)!
|
||||
|
||||
// we are directly setting these params in the `RandomAccessReader` struct
|
||||
csvr.default_cell = '*'
|
||||
csvr.empty_cell = 'EMPTY'
|
||||
|
||||
// read a single cell at the row 1 column 0
|
||||
println('[0,1] => ${csvr.get_cell(x: 0, y: 1)!}')
|
||||
// try a non existing cell. it will return `default_cell` string
|
||||
println('[0,4] => ${csvr.get_cell(x: 0, y: 4)!}')
|
||||
// try an empty string. it will return `empty_cell` string
|
||||
println('[1,2] => ${csvr.get_cell(x: 1, y: 2)!}')
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
[0,1] => 0
|
||||
[0,4] => *
|
||||
[1,2] => EMPTY
|
||||
```
|
||||
The function `csvr.get_cell()` allows reading a single cell as a `string`.
|
||||
|
||||
## Using the header
|
||||
This example reads the CSV file header (if present) to simplify
|
||||
some read operations on the CSV file.
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
const txt = '
|
||||
a,b,c
|
||||
0,pippo,1.2
|
||||
1,pero,2.3
|
||||
'
|
||||
|
||||
fn main() {
|
||||
mut csvr := csv.csv_reader_from_string(txt)!
|
||||
|
||||
// try to create the header, it need the header and
|
||||
// at least one row of data
|
||||
csvr.build_header_dict(csv.GetHeaderConf{})!
|
||||
println('Header: ${csvr.header_list}')
|
||||
|
||||
// simple cell read usign the header map
|
||||
println(csvr.get_cell(x: csvr.header_map['b'], y: 1)!)
|
||||
|
||||
// get typed value instead of a string
|
||||
println(csvr.get_cellt(x: csvr.header_map['b'], y: 2)! as string)
|
||||
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Header: [csv.HeaderItem{
|
||||
label: 'a'
|
||||
column: 0
|
||||
htype: int
|
||||
}, csv.HeaderItem{
|
||||
label: 'b'
|
||||
column: 1
|
||||
htype: string
|
||||
}, csv.HeaderItem{
|
||||
label: 'c'
|
||||
column: 2
|
||||
htype: f32
|
||||
}]
|
||||
pippo
|
||||
pero
|
||||
```
|
||||
In this example we have seen:
|
||||
* how to generate the header with `build_header_dict(csv.GetHeaderConf{})`
|
||||
* how to read a cell as a string with `get_cell(x:csvr.header_map['b'], y:1)`
|
||||
* how to read a cell as a sum type `CellValue` using `get_cellt(x:csvr.header_map['b'], y:2)`
|
||||
|
||||
The sum type `CellValue` is defined as the following:
|
||||
```
|
||||
type CellValue = int | f32 | string
|
||||
```
|
||||
## Read columns
|
||||
In this example we read two columns from a big file from disk:
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
fn main() {
|
||||
file_path := 'big2.csv'
|
||||
mut csvr := csv.csv_reader(
|
||||
file_path: file_path // path to the file CSV
|
||||
mem_buf_size: 1024 * 1024 * 64 // we set 64MByte of buffer for this file
|
||||
end_line_len: csv.endline_crlf_len // we are using a windows text file
|
||||
)!
|
||||
// The data will be saved in this array
|
||||
mut data := [][]string{len: csvr.csv_map.len}
|
||||
for row_index in 1 .. csvr.csv_map.len {
|
||||
// get single cells
|
||||
data[row_index] << csvr.get_cell(x: 2, y: row_index)!
|
||||
data[row_index] << csvr.get_cell(x: 3, y: row_index)!
|
||||
}
|
||||
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
This style of reading is faster then reading an entire row then extracting the needed data.
|
||||
|
||||
## Comments and blank lines
|
||||
This example shows how to handle blank rows and comment rows in the file:
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
const txt = '
|
||||
# this is a comment line
|
||||
a,b,c
|
||||
|
||||
|
||||
|
||||
0,1,2
|
||||
|
||||
3,4,5
|
||||
# another comment
|
||||
'
|
||||
|
||||
fn main() {
|
||||
mut csvr := csv.csv_reader(
|
||||
scr_buf: txt.str
|
||||
scr_buf_len: txt.len
|
||||
comment: `#` // line starting with # will be ignored
|
||||
)!
|
||||
// scan all rows, csvr.csv_map.len contain the valid
|
||||
// rows number in the CSV file.
|
||||
for row_index in 0 .. csvr.csv_map.len {
|
||||
row := csvr.get_row(row_index)!
|
||||
println(row)
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
['a', 'b', 'c']
|
||||
['0', '1', '2']
|
||||
['3', '4', '5']
|
||||
```
|
||||
## Quote char
|
||||
This example shows how to handle quoted cells:
|
||||
```v
|
||||
import encoding.csv
|
||||
|
||||
const txt = "
|
||||
# comment line
|
||||
'a' , 'b', 'c'
|
||||
'1' , '2', '3'
|
||||
'4' ,'5', 'a,b,c', 'e'
|
||||
"
|
||||
|
||||
fn main() {
|
||||
mut csvr := csv.csv_reader(
|
||||
scr_buf: txt.str // string pointer
|
||||
scr_buf_len: txt.len // string length
|
||||
comment: `#` // line starting with # will be ignored
|
||||
quote: `'` // char used for quotes
|
||||
quote_remove: true // remove quotes from the cells
|
||||
)!
|
||||
|
||||
// scan all rows, csvr.csv_map.len contain the valid
|
||||
// rows number in the CSV file.
|
||||
for row_index in 0 .. csvr.csv_map.len {
|
||||
row := csvr.get_row(row_index)!
|
||||
println(row)
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
['a', 'b', 'c']
|
||||
['1', '2', '3']
|
||||
['4', '5', 'a,b,c', 'e']
|
||||
```
|
||||
## Performance
|
||||
This module was tested with CSV files up to 4 GBs with 4 million rows
|
||||
@@ -0,0 +1,34 @@
|
||||
import encoding.csv
|
||||
|
||||
fn test_no_ending() {
|
||||
data := 'x,y,d
|
||||
a,b
|
||||
w'
|
||||
mut parser := csv.new_reader(data)
|
||||
mut arr := []string{}
|
||||
for {
|
||||
items := parser.read() or { break }
|
||||
arr << items.join('-')
|
||||
}
|
||||
dump(arr)
|
||||
assert arr[0].str() == 'x-y-d'
|
||||
assert arr[1].str() == 'a-b'
|
||||
assert arr[2].str() == 'w'
|
||||
}
|
||||
|
||||
fn test_with_ending() {
|
||||
data := 'x,y,d
|
||||
a,b
|
||||
w
|
||||
'
|
||||
mut parser := csv.new_reader(data)
|
||||
mut arr := []string{}
|
||||
for {
|
||||
items := parser.read() or { break }
|
||||
arr << items.join('-')
|
||||
}
|
||||
dump(arr)
|
||||
assert arr[0].str() == 'x-y-d'
|
||||
assert arr[1].str() == 'a-b'
|
||||
assert arr[2].str() == 'w'
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
/*
|
||||
csv random access reader 1.0 alpha
|
||||
|
||||
Copyright (c) 2023 Dario Deledda. All rights reserved.
|
||||
Use of this source code is governed by an MIT license
|
||||
that can be found in the LICENSE file.
|
||||
|
||||
Known limitations:
|
||||
- no stream reading
|
||||
*/
|
||||
module csv
|
||||
|
||||
import os
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Consts
|
||||
*
|
||||
******************************************************************************/
|
||||
// endline lengths
|
||||
pub const endline_cr_len = 1
|
||||
pub const endline_crlf_len = 2
|
||||
|
||||
// Type of read buffer
|
||||
pub const ram_csv = 1
|
||||
pub const file_csv = 0
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Structs
|
||||
*
|
||||
******************************************************************************/
|
||||
pub enum ColumType {
|
||||
string = 0
|
||||
int = 1
|
||||
f32 = 2
|
||||
}
|
||||
|
||||
pub struct HeaderItem {
|
||||
pub mut:
|
||||
label string
|
||||
column int
|
||||
htype ColumType = .string
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct RandomAccessReader {
|
||||
pub mut:
|
||||
index i64
|
||||
|
||||
f os.File
|
||||
f_len i64
|
||||
is_bom_present bool
|
||||
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
|
||||
end_line u8 = `\n`
|
||||
end_line_len int = endline_cr_len // size of the endline rune \n = 1, \r\n = 2
|
||||
separator u8 = `,` // comma is the default separator
|
||||
separator_len int = 1 // size of the separator rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
quote_remove bool // if true clear the cell from the quotes
|
||||
comment u8 = `#` // every line that start with the quote char is ignored
|
||||
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string = '#' // retunrn this if empty cell
|
||||
// ram buffer
|
||||
mem_buf_type u32 // buffer type 0=File,1=RAM
|
||||
mem_buf voidptr // buffer used to load chars from file
|
||||
mem_buf_size i64 // size of the buffer
|
||||
mem_buf_start i64 = -1 // start index in the file of the read buffer
|
||||
mem_buf_end i64 = -1 // end index in the file of the read buffer
|
||||
// csv map for quick access
|
||||
create_map_csv bool = true // flag to enable the csv map creation
|
||||
csv_map [][]i64
|
||||
// header
|
||||
header_row int = -1 // row index of the header in the csv_map
|
||||
header_list []HeaderItem // list of the header item
|
||||
header_map map[string]int // map from header label to column index
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RandomAccessReaderConfig {
|
||||
pub:
|
||||
scr_buf voidptr // pointer to the buffer of data
|
||||
scr_buf_len i64 // if > 0 use the RAM pointed from scr_buf as source of data
|
||||
file_path string
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
mem_buf_size int = 1024 * 64 // default buffer size 64KByte
|
||||
separator u8 = `,`
|
||||
comment u8 = `#` // every line that start with the quote char is ignored
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string // return this string if empty cell
|
||||
end_line_len int = endline_cr_len // size of the endline rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
quote_remove bool // if true clear the cell from the quotes
|
||||
create_map_csv bool = true // if true make the map of the csv file
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Init, dispose, fill buffer
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
// csv_reader_from_string create a csv reader from a string
|
||||
pub fn csv_reader_from_string(in_str string) !&RandomAccessReader {
|
||||
return csv_reader(RandomAccessReaderConfig{ scr_buf: in_str.str, scr_buf_len: in_str.len })!
|
||||
}
|
||||
|
||||
// csv_reader create a random access csv reader
|
||||
pub fn csv_reader(cfg RandomAccessReaderConfig) !&RandomAccessReader {
|
||||
mut cr := &RandomAccessReader{}
|
||||
|
||||
cr.start_index = cfg.start_index
|
||||
cr.end_index = cfg.end_index
|
||||
|
||||
if cfg.scr_buf != 0 && cfg.scr_buf_len > 0 {
|
||||
cr.mem_buf_type = ram_csv // RAM buffer
|
||||
cr.mem_buf = cfg.scr_buf
|
||||
cr.mem_buf_size = cfg.scr_buf_len
|
||||
if cfg.end_index == -1 {
|
||||
cr.end_index = cfg.scr_buf_len
|
||||
}
|
||||
|
||||
// check if BOM header is in the memory buffer
|
||||
unsafe {
|
||||
if *&u8(cr.mem_buf) == 0xEF && *(&u8(cr.mem_buf) + 1) == 0xBB
|
||||
&& *(&u8(cr.mem_buf) + 2) == 0xBF {
|
||||
cr.is_bom_present = true
|
||||
cr.index += 3 // skip the BOM
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if is a file source
|
||||
else if cfg.file_path.len > 0 {
|
||||
if !os.exists(cfg.file_path) {
|
||||
return error('ERROR: file ${cfg.file_path} not found!')
|
||||
}
|
||||
cr.mem_buf_type = file_csv // File buffer
|
||||
// allocate the memory
|
||||
unsafe {
|
||||
cr.mem_buf = malloc(cfg.mem_buf_size)
|
||||
cr.mem_buf_size = cfg.mem_buf_size
|
||||
}
|
||||
cr.f = os.open_file(cfg.file_path, 'rb')!
|
||||
|
||||
cr.f.seek(0, .end)!
|
||||
cr.f_len = cr.f.tell()!
|
||||
|
||||
cr.f.seek(cfg.start_index, .start)!
|
||||
cr.index = cr.f.tell()!
|
||||
|
||||
if cfg.end_index == -1 {
|
||||
cr.end_index = cr.f_len
|
||||
}
|
||||
|
||||
// check if BOM header is in the file
|
||||
if cr.index == 0 {
|
||||
if cr.f.read_into_ptr(cr.mem_buf, 4)! == 4 {
|
||||
unsafe {
|
||||
if *&u8(cr.mem_buf) == 0xEF && *(&u8(cr.mem_buf) + 1) == 0xBB
|
||||
&& *(&u8(cr.mem_buf) + 2) == 0xBF {
|
||||
cr.is_bom_present = true
|
||||
cr.index += 3 // skip the BOM
|
||||
}
|
||||
}
|
||||
}
|
||||
cr.f.seek(cfg.start_index, .start)!
|
||||
}
|
||||
}
|
||||
|
||||
cr.default_cell = cfg.default_cell
|
||||
cr.empty_cell = cfg.empty_cell
|
||||
cr.end_line_len = cfg.end_line_len
|
||||
cr.separator = cfg.separator
|
||||
cr.comment = cfg.comment
|
||||
cr.quote_remove = cfg.quote_remove
|
||||
cr.quote = cfg.quote
|
||||
|
||||
cr.create_map_csv = cfg.create_map_csv
|
||||
if cr.create_map_csv {
|
||||
cr.map_csv()!
|
||||
}
|
||||
|
||||
return cr
|
||||
}
|
||||
|
||||
// dispose_csv_reader release the resources used by the csv_reader
|
||||
pub fn (mut cr RandomAccessReader) dispose_csv_reader() {
|
||||
if cr.mem_buf_type == ram_csv {
|
||||
// do nothing, ram buffer is static
|
||||
} else if cr.mem_buf_type == file_csv {
|
||||
// file close
|
||||
if cr.f.is_opened {
|
||||
cr.f.close()
|
||||
}
|
||||
|
||||
// free the allocated memory
|
||||
if cr.mem_buf_size > 0 {
|
||||
unsafe {
|
||||
free(cr.mem_buf)
|
||||
}
|
||||
cr.mem_buf = unsafe { nil }
|
||||
cr.mem_buf_size = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut cr RandomAccessReader) fill_buffer(i i64) !i64 {
|
||||
// use ram
|
||||
if cr.mem_buf_type == ram_csv {
|
||||
// do nothing, ram buffer are static for now
|
||||
cr.mem_buf_start = i
|
||||
cr.mem_buf_end = cr.mem_buf_size
|
||||
read_bytes_count := cr.mem_buf_end - cr.mem_buf_start
|
||||
// println("fill_buffer RAM: ${i} read_bytes_count: ${read_bytes_count} mem_buf_start: ${cr.mem_buf_start} mem_buf_end: ${cr.mem_buf_end}")
|
||||
return i64(read_bytes_count)
|
||||
// use file
|
||||
} else if cr.mem_buf_type == file_csv {
|
||||
cr.start_index = i
|
||||
cr.f.seek(cr.start_index, .start)!
|
||||
// IMPORTANT: add 64 bit support in vlib!!
|
||||
read_bytes_count := cr.f.read_into_ptr(cr.mem_buf, int(cr.mem_buf_size))!
|
||||
cr.mem_buf_start = i
|
||||
cr.mem_buf_end = i + read_bytes_count
|
||||
// println("fill_buffer FILE: ${i} read_bytes_count: ${read_bytes_count} mem_buf_start: ${cr.mem_buf_start} mem_buf_end: ${cr.mem_buf_end}")
|
||||
return i64(read_bytes_count)
|
||||
}
|
||||
return i64(-1)
|
||||
}
|
||||
|
||||
// copy_configuration copies the configuration from another csv RandomAccessReader
|
||||
// this function is a helper for using the RandomAccessReader in multi threaded applications
|
||||
// pay attention to the free process
|
||||
pub fn (mut cr RandomAccessReader) copy_configuration(src_cr RandomAccessReader) {
|
||||
cr.header_row = src_cr.header_row
|
||||
unsafe {
|
||||
cr.header_list = &src_cr.header_list
|
||||
cr.header_map = &src_cr.header_map
|
||||
cr.csv_map = &src_cr.csv_map
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Csv mapper, mapped reader
|
||||
*
|
||||
******************************************************************************/
|
||||
// map_csv create an index of whole csv file to consent random access to every cell in the file
|
||||
pub fn (mut cr RandomAccessReader) map_csv() ! {
|
||||
mut count := 0
|
||||
mut i := i64(0)
|
||||
mut capture_flag := true
|
||||
mut drop_row := false
|
||||
mut quote_flag := false // true if we are parsing inside a quote
|
||||
|
||||
// if File return to the start of the file
|
||||
if cr.mem_buf_type == file_csv {
|
||||
cr.f.seek(cr.start_index, .start)!
|
||||
}
|
||||
|
||||
unsafe {
|
||||
p := &u8(cr.mem_buf)
|
||||
cr.csv_map << []i64{}
|
||||
cr.csv_map[0] << if cr.is_bom_present { 3 } else { 0 } // skip the BOM data
|
||||
|
||||
// mut counter := i64(0)
|
||||
for i < cr.end_index {
|
||||
read_bytes_count := cr.fill_buffer(i)!
|
||||
|
||||
// DEBUG print
|
||||
// perc := f32(counter) / f32(cr.end_index) * 100.0
|
||||
// println("${perc:.2f}")
|
||||
|
||||
// println("${i:-12d} of ${cr.f_len:-12d} readed: ${read_bytes_count}")
|
||||
mut p1 := p
|
||||
mut i1 := i64(0)
|
||||
for i1 < read_bytes_count {
|
||||
// println("loop char: ${*&u8(p1):c}")
|
||||
// manage quote char
|
||||
if *p1 == cr.quote {
|
||||
quote_flag = !quote_flag
|
||||
p1++
|
||||
i1++
|
||||
}
|
||||
else if // manage comment line
|
||||
!quote_flag && *p1 == cr.comment && cr.csv_map[cr.csv_map.len - 1].len <= 1 {
|
||||
drop_row = true
|
||||
p1++
|
||||
i1++
|
||||
// println("drop_row: ${cr.csv_map.len - 1}")
|
||||
}
|
||||
else if // capture separator
|
||||
!quote_flag && capture_flag && *p1 == cr.separator && !drop_row {
|
||||
cr.csv_map[cr.csv_map.len - 1] << (i + i1)
|
||||
|
||||
p1 += cr.separator_len
|
||||
i1 += cr.separator_len
|
||||
}
|
||||
else if // capture end line
|
||||
*p1 == cr.end_line {
|
||||
if quote_flag {
|
||||
error_col := cr.csv_map[cr.csv_map.len - 1].last() - cr.csv_map[cr.csv_map.len - 1].first()
|
||||
return error('ERROR: quote not closed at row ${count} after column ${error_col}!')
|
||||
}
|
||||
count++
|
||||
|
||||
cr.csv_map[cr.csv_map.len - 1] << (i + i1) - (cr.end_line_len - 1)
|
||||
p1 += cr.end_line_len
|
||||
i1 += cr.end_line_len
|
||||
|
||||
if drop_row == true {
|
||||
cr.csv_map[cr.csv_map.len - 1].clear()
|
||||
drop_row = false
|
||||
} else {
|
||||
// skip empty rows
|
||||
if cr.csv_map[cr.csv_map.len - 1].len == 2
|
||||
&& cr.csv_map[cr.csv_map.len - 1][0] == cr.csv_map[cr.csv_map.len - 1][1] {
|
||||
// recycle the row
|
||||
cr.csv_map[cr.csv_map.len - 1].clear()
|
||||
} else {
|
||||
// it all ok, insert a new row
|
||||
cr.csv_map << []i64{cap: cr.csv_map[cr.csv_map.len - 1].len}
|
||||
}
|
||||
}
|
||||
|
||||
cr.csv_map[cr.csv_map.len - 1] << (i + i1) - (cr.end_line_len - 1)
|
||||
|
||||
p1 -= (cr.end_line_len - 1)
|
||||
i1 -= (cr.end_line_len - 1)
|
||||
|
||||
// DEBUG checks
|
||||
// r := &u8(cr.mem_buf) + (i + i1) - (cr.end_line_len - 1)
|
||||
// r := p1
|
||||
// println("char: ${*r:c}")
|
||||
} else {
|
||||
p1++
|
||||
i1++
|
||||
}
|
||||
}
|
||||
i += read_bytes_count
|
||||
// counter += i1
|
||||
}
|
||||
}
|
||||
// remove last row if it is not a valid one
|
||||
if cr.csv_map[cr.csv_map.len - 1].len < 2 {
|
||||
cr.csv_map.delete(cr.csv_map.len - 1)
|
||||
}
|
||||
|
||||
// if File return to the start of the file
|
||||
if cr.mem_buf_type == file_csv {
|
||||
cr.f.seek(cr.start_index, .start)!
|
||||
}
|
||||
|
||||
// println("map_csv Done! ${count}")
|
||||
}
|
||||
|
||||
// get_row get a row from the CSV file as a string array
|
||||
pub fn (mut cr RandomAccessReader) get_row(y int) ![]string {
|
||||
mut h := []string{}
|
||||
if cr.csv_map.len > 1 {
|
||||
for x in 0 .. (cr.csv_map[y].len - 1) {
|
||||
h << cr.get_cell(x: x, y: y)!
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GetCellConfig {
|
||||
pub:
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
// get_cell read a single cel nd return a string
|
||||
pub fn (mut cr RandomAccessReader) get_cell(cfg GetCellConfig) !string {
|
||||
if cfg.y < cr.csv_map.len && cfg.x < (cr.csv_map[cfg.y].len - 1) {
|
||||
mut start := cr.csv_map[cfg.y][cfg.x]
|
||||
mut end := cr.csv_map[cfg.y][cfg.x + 1]
|
||||
|
||||
if cfg.x > 0 {
|
||||
start++
|
||||
}
|
||||
|
||||
mut len := end - start
|
||||
// println("len calc: ${len}")
|
||||
if len <= 0 {
|
||||
return cr.empty_cell
|
||||
}
|
||||
|
||||
// fill the buffer if needed
|
||||
if !(start >= cr.mem_buf_start && end < cr.mem_buf_end) {
|
||||
cr.fill_buffer(start)!
|
||||
}
|
||||
unsafe {
|
||||
// execute this section only if we need to remove the quotes
|
||||
if cr.quote_remove {
|
||||
// println("[${start},${end}] len:${len}")
|
||||
// remove front quote and spaces
|
||||
mut tmp_p := &u8(cr.mem_buf) + start - cr.start_index
|
||||
for start < end {
|
||||
if *tmp_p == cr.quote {
|
||||
start++
|
||||
break
|
||||
}
|
||||
start++
|
||||
tmp_p++
|
||||
}
|
||||
// println("after start quote filtering [${start},${end}] len:${len}")
|
||||
// remove back quote and spaces
|
||||
tmp_p = &u8(cr.mem_buf) + end - cr.start_index
|
||||
for end > start {
|
||||
if *tmp_p == cr.quote {
|
||||
break
|
||||
}
|
||||
tmp_p--
|
||||
end--
|
||||
}
|
||||
// println("after end quote filtering [${start},${end}] len:${len}")
|
||||
|
||||
len = end - start
|
||||
// println("len calc2: ${len}")
|
||||
if len <= 0 {
|
||||
return cr.empty_cell
|
||||
}
|
||||
// println("[${start},${end}] len:${len}")
|
||||
}
|
||||
|
||||
// create the string from the buffer
|
||||
mut tmp_mem := malloc_noscan(isize(len + 1))
|
||||
/*
|
||||
defer {
|
||||
free(tmp_mem)
|
||||
}
|
||||
*/
|
||||
mem_start := &u8(cr.mem_buf) + start - cr.start_index
|
||||
vmemcpy(tmp_mem, mem_start, isize(len))
|
||||
tmp_mem[len] = 0 // 0 for C string compatibility
|
||||
ret_str := tos(tmp_mem, int(len))
|
||||
return ret_str
|
||||
}
|
||||
}
|
||||
return cr.default_cell
|
||||
}
|
||||
|
||||
pub type CellValue = f32 | int | string
|
||||
|
||||
// get_cellt read a single cell and return a sum type CellValue
|
||||
pub fn (mut cr RandomAccessReader) get_cellt(cfg GetCellConfig) !CellValue {
|
||||
if cr.header_row >= 0 && cfg.x < cr.header_list.len {
|
||||
h := cr.header_list[cfg.x]
|
||||
res := cr.get_cell(cfg)!
|
||||
match h.htype {
|
||||
.int {
|
||||
return res.trim_space().int()
|
||||
}
|
||||
.string {
|
||||
return res
|
||||
}
|
||||
.f32 {
|
||||
return res.trim_space().f32()
|
||||
}
|
||||
}
|
||||
}
|
||||
return cr.get_cell(cfg)!
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Header management
|
||||
*
|
||||
******************************************************************************/
|
||||
@[params]
|
||||
pub struct GetHeaderConf {
|
||||
pub:
|
||||
header_row int // row where to inspect the header
|
||||
}
|
||||
|
||||
// build_header_dict infer the header, it use the first available row in not row number is passesd
|
||||
// it try to infer the type of column using the first available row after the header
|
||||
// By default all the column are of the string type
|
||||
pub fn (mut cr RandomAccessReader) build_header_dict(cfg GetHeaderConf) ! {
|
||||
if cr.csv_map.len > 1 && cfg.header_row >= 0 && cfg.header_row < cr.csv_map.len {
|
||||
cr.header_row = cfg.header_row
|
||||
for col in 0 .. (cr.csv_map[cfg.header_row].len - 1) {
|
||||
// fill the base struct
|
||||
label := cr.get_cell(x: col, y: cfg.header_row)!
|
||||
mut h := HeaderItem{
|
||||
label: label
|
||||
column: col
|
||||
htype: .string
|
||||
}
|
||||
|
||||
// try to infer the type if we haev at least one more row
|
||||
if cfg.header_row + 1 < cr.csv_map.len {
|
||||
x := cr.get_cell(x: col, y: cfg.header_row + 1)!.trim_space().to_lower()
|
||||
mut sign_c := int(0)
|
||||
mut int_c := int(0)
|
||||
mut float_c := int(0)
|
||||
mut alpha_c := int(0)
|
||||
mut htype := ColumType.string
|
||||
// raw extimation fo the type
|
||||
for c in x {
|
||||
if c in [`+`, `-`] {
|
||||
sign_c++
|
||||
continue
|
||||
}
|
||||
if c >= `0` && c <= `9` {
|
||||
int_c++
|
||||
continue
|
||||
}
|
||||
if c == `.` {
|
||||
float_c++
|
||||
continue
|
||||
}
|
||||
if c in [`e`, `E`] && (float_c > 0 || int_c > 0) {
|
||||
float_c++
|
||||
continue
|
||||
}
|
||||
alpha_c++
|
||||
break
|
||||
}
|
||||
|
||||
// if no alpha_c can be and int or a float
|
||||
if alpha_c == 0 {
|
||||
if float_c > 0 {
|
||||
htype = .f32
|
||||
} else {
|
||||
htype = .int
|
||||
}
|
||||
}
|
||||
h.htype = htype
|
||||
}
|
||||
|
||||
cr.header_list << h
|
||||
cr.header_map[label] = col
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Utility function
|
||||
*
|
||||
******************************************************************************/
|
||||
// rows_count count the rows in the csv between start_index and end_index
|
||||
pub fn (mut cr RandomAccessReader) rows_count() !i64 {
|
||||
mut count := i64(0)
|
||||
mut i := i64(0)
|
||||
|
||||
if cr.mem_buf_type == file_csv {
|
||||
cr.f.seek(cr.start_index, .start)!
|
||||
}
|
||||
unsafe {
|
||||
p := &u8(cr.mem_buf)
|
||||
for i < cr.end_index {
|
||||
read_bytes_count := cr.fill_buffer(i)!
|
||||
// println("${i:-12d} of ${cr.f_len:-12d} readed: ${read_bytes_count}")
|
||||
mut p1 := p
|
||||
mut i1 := 0
|
||||
for i1 < read_bytes_count {
|
||||
if *p1 == cr.end_line {
|
||||
count++
|
||||
}
|
||||
p1++
|
||||
i1++
|
||||
}
|
||||
i += read_bytes_count
|
||||
}
|
||||
}
|
||||
if cr.mem_buf_type == file_csv {
|
||||
cr.f.seek(cr.start_index, .start)!
|
||||
}
|
||||
// println("rows_count Done!")
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
csv serial reader 1.0 alpha
|
||||
|
||||
Copyright (c) 2023 Dario Deledda. All rights reserved.
|
||||
Use of this source code is governed by an MIT license
|
||||
that can be found in the LICENSE file.
|
||||
|
||||
Known limitations:
|
||||
*/
|
||||
module csv
|
||||
|
||||
import os
|
||||
|
||||
@[params]
|
||||
pub struct SequentialReaderConfig {
|
||||
pub:
|
||||
scr_buf voidptr // pointer to the buffer of data
|
||||
scr_buf_len i64 // if > 0 use the RAM pointed by scr_buf as source of data
|
||||
file_path string
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
mem_buf_size int = 1024 * 64 // default buffer size 64KByte
|
||||
separator u8 = `,`
|
||||
comment u8 = `#` // every line that start with the comment char is ignored
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string // return this string if empty cell
|
||||
end_line_len int = endline_cr_len // size of the endline rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
}
|
||||
|
||||
pub struct SequentialReader {
|
||||
pub mut:
|
||||
index i64
|
||||
|
||||
f os.File
|
||||
f_len i64
|
||||
is_bom_present bool
|
||||
|
||||
start_index i64
|
||||
end_index i64 = -1
|
||||
|
||||
end_line u8 = `\n`
|
||||
end_line_len int = endline_cr_len // size of the endline rune \n = 1, \r\n = 2
|
||||
separator u8 = `,` // comma is the default separator
|
||||
separator_len int = 1 // size of the separator rune
|
||||
quote u8 = `"` // double quote is the standard quote char
|
||||
|
||||
comment u8 = `#` // every line that start with the quote char is ignored
|
||||
|
||||
default_cell string = '*' // return this string if out of the csv boundaries
|
||||
empty_cell string = '#' // retunrn this if empty cell
|
||||
// ram buffer
|
||||
mem_buf_type u32 // buffer type 0=File,1=RAM
|
||||
mem_buf voidptr // buffer used to load chars from file
|
||||
mem_buf_size i64 // size of the buffer
|
||||
mem_buf_start i64 = -1 // start index in the file of the read buffer
|
||||
mem_buf_end i64 = -1 // end index in the file of the read buffer
|
||||
|
||||
ch_buf []u8 = []u8{cap: 1024}
|
||||
// error management
|
||||
row_count i64
|
||||
col_count i64
|
||||
}
|
||||
|
||||
// csv_sequential_reader creates a sequential csv reader
|
||||
pub fn csv_sequential_reader(cfg SequentialReaderConfig) !&SequentialReader {
|
||||
mut cr := &SequentialReader{}
|
||||
|
||||
cr.start_index = cfg.start_index
|
||||
cr.end_index = cfg.end_index
|
||||
|
||||
// reading from a RAM buffer
|
||||
if cfg.scr_buf != 0 && cfg.scr_buf_len > 0 {
|
||||
cr.mem_buf_type = ram_csv // RAM buffer
|
||||
cr.mem_buf = cfg.scr_buf
|
||||
cr.mem_buf_size = cfg.scr_buf_len
|
||||
if cfg.end_index == -1 {
|
||||
cr.end_index = cfg.scr_buf_len
|
||||
}
|
||||
|
||||
// check if BOM header is in the memory buffer
|
||||
unsafe {
|
||||
if *&u8(cr.mem_buf) == 0xEF && *(&u8(cr.mem_buf) + 1) == 0xBB
|
||||
&& *(&u8(cr.mem_buf) + 2) == 0xBF {
|
||||
cr.is_bom_present = true
|
||||
cr.index += 3 // skip the BOM
|
||||
cr.start_index += 3 // skip the BOM
|
||||
}
|
||||
}
|
||||
cr.mem_buf_start = 0
|
||||
cr.mem_buf_end = cr.mem_buf_size
|
||||
|
||||
// check if is a file source
|
||||
} else if cfg.file_path.len > 0 {
|
||||
if !os.exists(cfg.file_path) {
|
||||
return error('ERROR: file ${cfg.file_path} not found!')
|
||||
}
|
||||
cr.mem_buf_type = file_csv // File buffer
|
||||
// allocate the memory
|
||||
unsafe {
|
||||
cr.mem_buf = malloc(cfg.mem_buf_size)
|
||||
cr.mem_buf_size = cfg.mem_buf_size
|
||||
}
|
||||
cr.f = os.open_file(cfg.file_path, 'rb')!
|
||||
|
||||
cr.f.seek(0, .end)!
|
||||
cr.f_len = cr.f.tell()!
|
||||
|
||||
cr.f.seek(cfg.start_index, .start)!
|
||||
cr.index = cr.f.tell()!
|
||||
|
||||
if cfg.end_index == -1 {
|
||||
cr.end_index = cr.f_len
|
||||
}
|
||||
|
||||
// check if BOM header is in the file
|
||||
if cr.index == 0 {
|
||||
if cr.f.read_into_ptr(cr.mem_buf, 4)! == 4 {
|
||||
unsafe {
|
||||
if *&u8(cr.mem_buf) == 0xEF && *(&u8(cr.mem_buf) + 1) == 0xBB
|
||||
&& *(&u8(cr.mem_buf) + 2) == 0xBF {
|
||||
cr.is_bom_present = true
|
||||
cr.index += 3 // skip the BOM
|
||||
cr.start_index += 3 // skip the BOM
|
||||
}
|
||||
}
|
||||
}
|
||||
cr.f.seek(cfg.start_index, .start)!
|
||||
}
|
||||
}
|
||||
|
||||
cr.default_cell = cfg.default_cell
|
||||
cr.empty_cell = cfg.empty_cell
|
||||
cr.end_line_len = cfg.end_line_len
|
||||
cr.separator = cfg.separator
|
||||
cr.comment = cfg.comment
|
||||
cr.quote = cfg.quote
|
||||
|
||||
return cr
|
||||
}
|
||||
|
||||
// dispose_csv_reader release the resources used by the csv_reader
|
||||
pub fn (mut cr SequentialReader) dispose_csv_reader() {
|
||||
if cr.mem_buf_type == ram_csv {
|
||||
// do nothing, ram buffer is static
|
||||
} else if cr.mem_buf_type == file_csv {
|
||||
// file close
|
||||
if cr.f.is_opened {
|
||||
cr.f.close()
|
||||
}
|
||||
|
||||
// free the allocated memory
|
||||
if cr.mem_buf_size > 0 {
|
||||
unsafe {
|
||||
free(cr.mem_buf)
|
||||
}
|
||||
cr.mem_buf = unsafe { nil }
|
||||
cr.mem_buf_size = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// has_data return the bytes available for future readings
|
||||
pub fn (mut cr SequentialReader) has_data() i64 {
|
||||
return cr.end_index - cr.start_index
|
||||
}
|
||||
|
||||
fn (mut cr SequentialReader) fill_buffer(index i64) ! {
|
||||
if cr.mem_buf_type == ram_csv {
|
||||
// for now do nothing if ram buffer
|
||||
} else {
|
||||
cr.f.seek(index, .start)!
|
||||
// IMPORTANT: add 64 bit support in vlib!!
|
||||
read_bytes_count := cr.f.read_into_ptr(cr.mem_buf, int(cr.mem_buf_size))!
|
||||
cr.mem_buf_start = index
|
||||
cr.mem_buf_end = index + read_bytes_count
|
||||
}
|
||||
}
|
||||
|
||||
enum SequentialReadingState as u16 {
|
||||
comment
|
||||
quote
|
||||
after_quote
|
||||
cell
|
||||
newline
|
||||
}
|
||||
|
||||
// get_next_row get the next row from the CSV file as a string array
|
||||
pub fn (mut cr SequentialReader) get_next_row() ![]string {
|
||||
mut row_res := []string{}
|
||||
// clear the cell buffer
|
||||
cr.ch_buf.clear()
|
||||
mut i := cr.start_index
|
||||
mut state := SequentialReadingState.cell
|
||||
|
||||
p := &u8(cr.mem_buf)
|
||||
for i < cr.end_index {
|
||||
if i < cr.mem_buf_start || i >= cr.mem_buf_end {
|
||||
cr.fill_buffer(i)!
|
||||
}
|
||||
unsafe {
|
||||
ch := *(p + i - cr.mem_buf_start)
|
||||
|
||||
if state == .cell {
|
||||
if ch == cr.separator {
|
||||
// must be optimized
|
||||
cr.ch_buf << 0
|
||||
row_res << if (cr.ch_buf.len - 1) == 0 {
|
||||
cr.empty_cell
|
||||
} else {
|
||||
(tos(cr.ch_buf.data, cr.ch_buf.len - 1).clone())
|
||||
}
|
||||
cr.ch_buf.clear()
|
||||
} else if cr.ch_buf.len == 0 && ch == cr.comment && row_res.len == 0 {
|
||||
state = .comment
|
||||
} else if ch == cr.quote {
|
||||
state = .quote
|
||||
cr.ch_buf.clear()
|
||||
cr.col_count++
|
||||
i++
|
||||
continue
|
||||
} else if ch == cr.end_line {
|
||||
cr.row_count++
|
||||
cr.col_count = 0
|
||||
|
||||
// skip empty rows
|
||||
if !(row_res.len == 0 && cr.ch_buf.len < 1) {
|
||||
cr.ch_buf << 0
|
||||
row_res << if (cr.ch_buf.len - 1) == 0 {
|
||||
cr.empty_cell
|
||||
} else {
|
||||
(tos(cr.ch_buf.data, cr.ch_buf.len - 1).clone())
|
||||
}
|
||||
i += cr.end_line_len - 1
|
||||
break
|
||||
}
|
||||
} else if ch == `\r` && cr.end_line_len == 2 {
|
||||
// skip CR
|
||||
} else { // normal char inside a cell
|
||||
cr.ch_buf << ch
|
||||
}
|
||||
}
|
||||
|
||||
if state == .comment {
|
||||
if cr.ch_buf.len > 0 {
|
||||
// must be optimized
|
||||
cr.ch_buf << 0
|
||||
row_res << if (cr.ch_buf.len - 1) == 0 {
|
||||
cr.empty_cell
|
||||
} else {
|
||||
(tos(cr.ch_buf.data, cr.ch_buf.len - 1).clone())
|
||||
}
|
||||
cr.ch_buf.clear()
|
||||
} else if ch == cr.end_line {
|
||||
state = .cell
|
||||
}
|
||||
}
|
||||
|
||||
if state == .quote {
|
||||
if ch == cr.quote {
|
||||
// must be optimized
|
||||
cr.ch_buf << 0
|
||||
row_res << if (cr.ch_buf.len - 1) == 0 {
|
||||
cr.empty_cell
|
||||
} else {
|
||||
(tos(cr.ch_buf.data, cr.ch_buf.len - 1).clone())
|
||||
}
|
||||
cr.ch_buf.clear()
|
||||
|
||||
state = .after_quote
|
||||
cr.col_count++
|
||||
i++
|
||||
continue
|
||||
} else if ch == cr.end_line {
|
||||
return error('ERROR: quote not closed at row ${cr.row_count} after column ${cr.col_count}!')
|
||||
} else { // normal char inside a quote inside a cell
|
||||
cr.ch_buf << ch
|
||||
}
|
||||
}
|
||||
|
||||
if state == .after_quote {
|
||||
if ch == cr.separator {
|
||||
state = .cell
|
||||
} else if ch == cr.end_line {
|
||||
cr.row_count++
|
||||
cr.col_count = 0
|
||||
cr.ch_buf.clear()
|
||||
i += cr.end_line_len - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
cr.col_count++
|
||||
i++
|
||||
}
|
||||
cr.start_index = i
|
||||
return row_res
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
csv reader 1.0 alpha
|
||||
|
||||
Copyright (c) 2023 Dario Deledda. All rights reserved.
|
||||
Use of this source code is governed by an MIT license
|
||||
that can be found in the LICENSE file.
|
||||
|
||||
This file contains tests
|
||||
|
||||
Known limitations:
|
||||
*/
|
||||
import encoding.csv
|
||||
import strings
|
||||
import os
|
||||
import rand
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Test Data
|
||||
*
|
||||
******************************************************************************/
|
||||
// dataset 1
|
||||
const txt1 = '
|
||||
|
||||
#
|
||||
# pippo
|
||||
#
|
||||
a,b,c,d,e,f,g
|
||||
0,dario,.2,3.2e-2,4,"pero5",6
|
||||
# first comment, test @# again
|
||||
1,2,3,4,5,6,7
|
||||
2,3,4,5,6,7,8
|
||||
3,4,5,6,7,8,9
|
||||
|
||||
a,"b,c,d",0,#,3,"pippo"
|
||||
|
||||
# last comment
|
||||
'
|
||||
|
||||
const target_header_list = [
|
||||
csv.HeaderItem{
|
||||
label: 'a'
|
||||
column: 0
|
||||
htype: .int
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'b'
|
||||
column: 1
|
||||
htype: .string
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'c'
|
||||
column: 2
|
||||
htype: .f32
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'd'
|
||||
column: 3
|
||||
htype: .f32
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'e'
|
||||
column: 4
|
||||
htype: .int
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'f'
|
||||
column: 5
|
||||
htype: .string
|
||||
},
|
||||
csv.HeaderItem{
|
||||
label: 'g'
|
||||
column: 6
|
||||
htype: .int
|
||||
},
|
||||
]
|
||||
|
||||
const target_data = [
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
['0', 'dario', '.2', '3.2e-2', '4', '"pero5"', '6'],
|
||||
['1', '2', '3', '4', '5', '6', '7'],
|
||||
['2', '3', '4', '5', '6', '7', '8'],
|
||||
['3', '4', '5', '6', '7', '8', '9'],
|
||||
['a', '"b,c,d"', '0', '#', '3', '"pippo"'], // 6 columns for test purpose
|
||||
]
|
||||
|
||||
// dataset 2 crlf string from windows
|
||||
const txt2 = '
|
||||
|
||||
#
|
||||
# pippo
|
||||
#
|
||||
a,b,c,d,e,f,g
|
||||
0,dario,.2,3.2e-2,4,"pero5",6
|
||||
# first comment, test @# again
|
||||
1,2,3,4,5,6,7
|
||||
2,3,4,5,6,7,8
|
||||
3,4,5,6,7,8,9
|
||||
|
||||
a,"b,c,d",,#,3,"pippo"
|
||||
|
||||
# last comment
|
||||
'
|
||||
|
||||
// dataset 3/4
|
||||
const txt3 = 'a,b,c,d\r\n0,1,2,3\r\n4,5,6,7\r\n'
|
||||
const txt4 = 'a,b,c,d\n0,1,2,3\n4,5,6,7\n'
|
||||
/******************************************************************************
|
||||
*
|
||||
* Test Sequential Functions
|
||||
*
|
||||
******************************************************************************/
|
||||
fn test_csv_sequential() {
|
||||
mut csvr := csv.csv_sequential_reader(scr_buf: txt1.str, scr_buf_len: txt1.len)!
|
||||
mut data := [][]string{}
|
||||
for csvr.has_data() > 1 {
|
||||
data << csvr.get_next_row()!
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
assert data[0][0] == 'a', 'test_csv_sequential1 reading failed!'
|
||||
// there is a final empty row in txt1
|
||||
assert data[data.len - 2][0] == 'a', 'test_csv_sequential2 reading failed!'
|
||||
assert data[data.len - 2][1] == 'b,c,d', 'test_csv_sequential3 reading failed!'
|
||||
|
||||
csvr = csv.csv_sequential_reader(scr_buf: txt2.str, scr_buf_len: txt2.len)!
|
||||
csvr.empty_cell = '####'
|
||||
data = [][]string{}
|
||||
for csvr.has_data() > 1 {
|
||||
data << csvr.get_next_row()!
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
assert data[data.len - 2][2] == '####', 'test_csv_sequential4 reading failed!'
|
||||
assert data[data.len - 2][5] == 'pippo', 'test_csv_sequential5 reading failed!'
|
||||
|
||||
// create a temp file to test csv parsing from file
|
||||
file_path_str := os.join_path(os.temp_dir(), 'test_csv.csv')
|
||||
// println("file_path_str: ${file_path_str}")
|
||||
|
||||
// test Windows confguration
|
||||
mut tmp_txt1 := txt1.replace('\n', '\r\n')
|
||||
|
||||
mut f := os.open_file(file_path_str, 'wb')!
|
||||
unsafe {
|
||||
f.write_ptr(tmp_txt1.str, tmp_txt1.len)
|
||||
}
|
||||
// f.write_string(tmp_txt1)!
|
||||
f.close()
|
||||
|
||||
csvr = csv.csv_sequential_reader(
|
||||
file_path: file_path_str
|
||||
mem_buf_size: 64
|
||||
end_line_len: csv.endline_crlf_len
|
||||
)!
|
||||
data = [][]string{}
|
||||
for csvr.has_data() > 1 {
|
||||
data << csvr.get_next_row()!
|
||||
}
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
assert data[0][0] == 'a', 'test_csv_sequential1 reading failed!'
|
||||
// there is a final empty row in txt1
|
||||
assert data[data.len - 2][0] == 'a', 'test_csv_sequential2 reading failed!'
|
||||
assert data[data.len - 2][1] == 'b,c,d', 'test_csv_sequential3 reading failed!'
|
||||
|
||||
// remove the temp file
|
||||
os.rm(file_path_str)!
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
*
|
||||
* Test Random Access Functions
|
||||
*
|
||||
******************************************************************************/
|
||||
fn perform_test(mut csvr csv.RandomAccessReader) ! {
|
||||
csvr.build_header_dict(csv.GetHeaderConf{})!
|
||||
|
||||
// test the Header reader
|
||||
// println("csvr.header_list: ${csvr.header_list}")
|
||||
assert csvr.header_list == target_header_list, 'header_list not matched!'
|
||||
|
||||
/*
|
||||
println("--------------------------------")
|
||||
for x in csvr.csv_map#[..5] {
|
||||
println(x.len)
|
||||
println(x)
|
||||
}
|
||||
println("--------------------------------")
|
||||
*/
|
||||
|
||||
// test the data reading
|
||||
mut data := [][]string{len: csvr.csv_map.len}
|
||||
for x in 0 .. csvr.csv_map.len {
|
||||
data[x] = csvr.get_row(x)!
|
||||
// if x % 10000 == 0 {
|
||||
// println("#${x:-6d}")
|
||||
//}
|
||||
}
|
||||
|
||||
/*
|
||||
// debug print
|
||||
println("---------------")
|
||||
for x in 0..csvr.csv_map.len {
|
||||
println(csvr.get_row(x)!)
|
||||
}
|
||||
*/
|
||||
|
||||
// test if we have the same amount of data rows
|
||||
assert data.len == csvr.csv_map.len, 'data len not equal'
|
||||
|
||||
// test the data retriever
|
||||
for row_count, row in target_data {
|
||||
// println("${data[row_count]} ${row}")
|
||||
assert data[row_count] == row, ''
|
||||
}
|
||||
|
||||
// test lfcr cr
|
||||
assert csvr.get_cell(x: 6, y: 4)! == '9'
|
||||
|
||||
// test the get cell behaviour
|
||||
assert csvr.get_cell(x: csvr.header_map['b'], y: 1)! == 'dario', 'get_cell failed 1'
|
||||
assert csvr.get_cell(x: csvr.header_map['g'], y: 5)! == csvr.default_cell, 'get_cell out of data failed 2'
|
||||
assert csvr.get_cellt(x: 0, y: 1)! == csv.CellValue(0), 'get_cellt [int] failed'
|
||||
assert csvr.get_cellt(x: 1, y: 1)! == csv.CellValue('dario'), 'get_cellt [string] failed'
|
||||
assert csvr.get_cellt(x: 2, y: 1)! == csv.CellValue(f32(.2)), 'get_cell [f32] failed'
|
||||
|
||||
// test the filter quote flag
|
||||
csvr.quote_remove = true
|
||||
assert csvr.get_cell(x: 1, y: 5)! == 'b,c,d', 'get_cell filer quote flag failed'
|
||||
}
|
||||
|
||||
fn perform_test2(mut csvr csv.RandomAccessReader) ! {
|
||||
csvr.build_header_dict(csv.GetHeaderConf{})!
|
||||
// test the empty cells
|
||||
assert csvr.get_cell(x: csvr.header_map['c'], y: 5)! == csvr.empty_cell, 'get_cell empty_cell failed 2'
|
||||
}
|
||||
|
||||
fn perform_test3(mut csvr csv.RandomAccessReader) ! {
|
||||
csvr.build_header_dict(csv.GetHeaderConf{})!
|
||||
/*
|
||||
// debug print
|
||||
println("---------------")
|
||||
for x in 0..csvr.csv_map.len {
|
||||
println(csvr.get_row(x)!)
|
||||
}
|
||||
*/
|
||||
assert csvr.get_cell(x: csvr.header_map['d'], y: 2)! == '7', 'test \n \r\n failed'
|
||||
}
|
||||
|
||||
fn test_csv_string() {
|
||||
// test the csv parsing from RAM
|
||||
mut csvr := csv.csv_reader_from_string(txt1)!
|
||||
perform_test(mut csvr)!
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
// create a temp file to test csv parsing from file
|
||||
file_path_str := os.join_path(os.temp_dir(), 'test_csv.csv')
|
||||
// println("file_path_str: ${file_path_str}")
|
||||
|
||||
// test Windows confguration
|
||||
mut tmp_txt1 := txt1.replace('\n', '\r\n')
|
||||
|
||||
mut f := os.open_file(file_path_str, 'wb')!
|
||||
unsafe {
|
||||
f.write_ptr(tmp_txt1.str, tmp_txt1.len)
|
||||
}
|
||||
// f.write_string(tmp_txt1)!
|
||||
f.close()
|
||||
|
||||
// parse the temp file
|
||||
csvr = csv.csv_reader(
|
||||
file_path: file_path_str
|
||||
mem_buf_size: 32
|
||||
end_line_len: csv.endline_crlf_len
|
||||
)!
|
||||
perform_test(mut csvr)!
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
// remove the temp file
|
||||
os.rm(file_path_str)!
|
||||
|
||||
csvr = csv.csv_reader_from_string(txt2)!
|
||||
perform_test2(mut csvr)!
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
// test crlf endline
|
||||
csvr = csv.csv_reader(
|
||||
scr_buf: txt3.str
|
||||
scr_buf_len: txt3.len
|
||||
end_line_len: csv.endline_crlf_len
|
||||
)!
|
||||
perform_test3(mut csvr)!
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
// test cr endline
|
||||
csvr =
|
||||
csv.csv_reader(scr_buf: txt4.str, scr_buf_len: txt4.len, end_line_len: csv.endline_cr_len)!
|
||||
perform_test3(mut csvr)!
|
||||
csvr.dispose_csv_reader()
|
||||
}
|
||||
|
||||
fn test_coherence() {
|
||||
file_path_str := os.join_path(os.temp_dir(), 'test_csv.csv')
|
||||
mut f := os.open_file(file_path_str, 'w')!
|
||||
mut b := strings.new_builder(64536)
|
||||
mut i := u64(0)
|
||||
mut sum := u64(0)
|
||||
for rows in 0 .. 1000 {
|
||||
for col in 0 .. 1000 {
|
||||
if col > 0 {
|
||||
b.write_u8(`,`)
|
||||
}
|
||||
b.write_string(i.str())
|
||||
i++
|
||||
sum += i
|
||||
}
|
||||
b.write_string('\n')
|
||||
}
|
||||
f.write_string(b.str())!
|
||||
f.close()
|
||||
|
||||
sum -= i
|
||||
// println('sum: ${sum}')
|
||||
|
||||
// parse the temp file
|
||||
mut csvr := csv.csv_reader(
|
||||
file_path: file_path_str
|
||||
mem_buf_size: 32
|
||||
end_line_len: csv.endline_cr_len
|
||||
)!
|
||||
|
||||
mut sum1 := u64(0)
|
||||
for row_index in 0 .. csvr.csv_map.len {
|
||||
row := csvr.get_row(row_index)!
|
||||
for x in row {
|
||||
sum1 += u64(x.int())
|
||||
}
|
||||
}
|
||||
// println('sum: ${sum1}')
|
||||
|
||||
csvr.dispose_csv_reader()
|
||||
|
||||
// remove the temp file
|
||||
os.rm(file_path_str)!
|
||||
|
||||
assert sum == sum1, 'csv coherence test failed'
|
||||
}
|
||||
|
||||
// Debug code
|
||||
fn main() {
|
||||
test_csv_string()
|
||||
}
|
||||
|
||||
// Multithreaded tests
|
||||
|
||||
fn create_csv(file_path string, size int) !i64 {
|
||||
// create csv file for the test
|
||||
mut csv_txt := 'pippo,count,count1,pera,sempronio,float'
|
||||
|
||||
mut f := os.open_file(file_path, 'w')!
|
||||
f.write_string(csv_txt + '\n')!
|
||||
mut count := i64(0)
|
||||
for i in 0 .. size {
|
||||
tmp := "${rand.int()}, ${i}, 3, \"txt1${i}\", \"txt2${i}\", ${f32(rand.u32()) / 1000.0}\n"
|
||||
f.write_string(tmp)!
|
||||
// if i % 1_000_000 == 0 {
|
||||
// println(i)
|
||||
// }
|
||||
count += i
|
||||
}
|
||||
f.close()
|
||||
return count
|
||||
}
|
||||
|
||||
fn read_lines(id int, csvr csv.RandomAccessReader, mut data [][]csv.CellValue, start_row int, end_row int) {
|
||||
// println(" func ${data.len},${data[1].len}")
|
||||
unsafe {
|
||||
for count, col_elem in csvr.header_list {
|
||||
// println("Check: ${col_elem}")
|
||||
match col_elem.htype {
|
||||
.string {
|
||||
// println('id:${id} String here')
|
||||
for row_index in start_row .. end_row {
|
||||
// println("str ${count},${row_index}")
|
||||
data[count][row_index - 1] = csvr.get_cell(x: count, y: row_index) or {
|
||||
panic('Str get_cell failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
.int {
|
||||
// println('id:${id} Int here')
|
||||
for row_index in start_row .. end_row {
|
||||
// println("int ${count},${row_index}")
|
||||
data[count][row_index - 1] = csvr.get_cell(x: count, y: row_index) or {
|
||||
panic('Int get_cell failed')
|
||||
}.trim_space().int()
|
||||
}
|
||||
}
|
||||
.f32 {
|
||||
// println('id:${id} f32 here')
|
||||
for row_index in start_row .. end_row {
|
||||
// println("f32 ${count},${row_index}")
|
||||
data[count][row_index - 1] = csvr.get_cell(x: count, y: row_index) or {
|
||||
panic('F32 get_cell failed')
|
||||
}.trim_space().f32()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // unsafe
|
||||
}
|
||||
|
||||
fn test_multithreading() {
|
||||
file_path_str := os.join_path(os.temp_dir(), 'test_csv.csv')
|
||||
size := 10_000
|
||||
|
||||
// create the test file
|
||||
res_count := create_csv(file_path_str, size)!
|
||||
|
||||
slices := 2 // number of slice of the csv
|
||||
mem_buf_size := 1024 * 1024 * 1
|
||||
|
||||
mut csvr := []csv.RandomAccessReader{}
|
||||
|
||||
// init first csv reader
|
||||
csvr << csv.csv_reader(file_path: file_path_str, mem_buf_size: mem_buf_size)!
|
||||
csvr[0].build_header_dict(csv.GetHeaderConf{})!
|
||||
|
||||
// init other csv readers using the first reader configuration
|
||||
for _ in 1 .. slices {
|
||||
mut tmp_csvr := csv.csv_reader(
|
||||
file_path: file_path_str
|
||||
mem_buf_size: mem_buf_size
|
||||
create_map_csv: false
|
||||
)!
|
||||
tmp_csvr.copy_configuration(csvr[0])
|
||||
csvr << tmp_csvr
|
||||
}
|
||||
|
||||
// read the data from the csv file
|
||||
mut data := [][]csv.CellValue{}
|
||||
|
||||
n_rows := csvr[0].csv_map.len
|
||||
unsafe {
|
||||
data = [][]csv.CellValue{len: csvr[0].header_list.len, init: []csv.CellValue{len: n_rows}}
|
||||
}
|
||||
step := n_rows / slices
|
||||
mut start := 1
|
||||
mut end := if (start + step) > n_rows { n_rows } else { start + step }
|
||||
|
||||
mut threads := []thread{}
|
||||
for task_index in 0 .. slices {
|
||||
threads << spawn read_lines(task_index, csvr[task_index], mut &data, start, end)
|
||||
start = end
|
||||
end = if (start + step) > n_rows { n_rows } else { start + step }
|
||||
}
|
||||
threads.wait()
|
||||
|
||||
// release the csv readers
|
||||
for mut item in csvr {
|
||||
item.dispose_csv_reader()
|
||||
}
|
||||
|
||||
// check for the integer column sum
|
||||
mut ck_count := i64(0)
|
||||
for i in 0 .. csvr[0].csv_map.len - 1 {
|
||||
ck_count += data[1][i] as int
|
||||
}
|
||||
|
||||
assert ck_count == res_count, 'check on csv file failed!'
|
||||
|
||||
// remove the temp file
|
||||
os.rm(file_path_str)!
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module csv
|
||||
|
||||
// Once interfaces are further along the idea would be to have something similar to
|
||||
// go's io.reader & bufio.reader rather than reading the whole file into string, this
|
||||
// would then satisfy that interface. I designed it this way to be easily adapted.
|
||||
struct CommentIsDelimiterError {
|
||||
Error
|
||||
}
|
||||
|
||||
fn (err CommentIsDelimiterError) msg() string {
|
||||
return 'encoding.csv: comment cannot be the same as delimiter'
|
||||
}
|
||||
|
||||
struct InvalidDelimiterError {
|
||||
Error
|
||||
}
|
||||
|
||||
fn (err InvalidDelimiterError) msg() string {
|
||||
return 'encoding.csv: invalid delimiter'
|
||||
}
|
||||
|
||||
struct EndOfFileError {
|
||||
Error
|
||||
}
|
||||
|
||||
fn (err EndOfFileError) msg() string {
|
||||
return 'encoding.csv: end of file'
|
||||
}
|
||||
|
||||
struct InvalidLineEndingError {
|
||||
Error
|
||||
}
|
||||
|
||||
fn (err InvalidLineEndingError) msg() string {
|
||||
return 'encoding.csv: could not find any valid line endings'
|
||||
}
|
||||
|
||||
struct Reader {
|
||||
// not used yet
|
||||
// has_header bool
|
||||
// headings []string
|
||||
data string
|
||||
delimiter u8
|
||||
comment u8
|
||||
mut:
|
||||
is_mac_pre_osx_le bool
|
||||
row_pos int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ReaderConfig {
|
||||
pub:
|
||||
delimiter u8 = `,`
|
||||
comment u8 = `#`
|
||||
}
|
||||
|
||||
// new_reader initializes a Reader with string data to parse and,
|
||||
// optionally, a custom delimiter.
|
||||
pub fn new_reader(data string, config ReaderConfig) &Reader {
|
||||
return &Reader{
|
||||
data: data
|
||||
delimiter: config.delimiter
|
||||
comment: config.comment
|
||||
}
|
||||
}
|
||||
|
||||
// read reads a row from the CSV data.
|
||||
// If successful, the result holds an array of each column's data.
|
||||
pub fn (mut r Reader) read() ![]string {
|
||||
l := r.read_record()!
|
||||
return l
|
||||
}
|
||||
|
||||
// Once we have multi dimensional array
|
||||
// pub fn (mut r Reader) read_all() ?[][]string {
|
||||
// mut records := []string{}
|
||||
// for {
|
||||
// record := r.read_record() or {
|
||||
// if err.error == err_eof.error {
|
||||
// return records
|
||||
// } else {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// records << record
|
||||
// }
|
||||
// return records
|
||||
// }
|
||||
fn (mut r Reader) read_line() !string {
|
||||
// last record
|
||||
if r.row_pos >= r.data.len {
|
||||
return &EndOfFileError{}
|
||||
}
|
||||
le := if r.is_mac_pre_osx_le { '\r' } else { '\n' }
|
||||
mut i := r.data.index_after(le, r.row_pos) or { -1 }
|
||||
if i == -1 {
|
||||
if r.row_pos == 0 {
|
||||
// check for pre osx mac line endings
|
||||
i = r.data.index_after('\r', r.row_pos) or { -1 }
|
||||
if i != -1 {
|
||||
r.is_mac_pre_osx_le = true
|
||||
} else {
|
||||
// no valid line endings found
|
||||
return &InvalidLineEndingError{}
|
||||
}
|
||||
} else {
|
||||
// No line ending on file
|
||||
i = r.data.len
|
||||
}
|
||||
}
|
||||
mut line := r.data[r.row_pos..i]
|
||||
r.row_pos = i + 1
|
||||
// normalize win line endings (remove extra \r)
|
||||
if !r.is_mac_pre_osx_le && (line.len >= 1 && line[line.len - 1] == `\r`) {
|
||||
line = line[..line.len - 1]
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
fn (mut r Reader) read_record() ![]string {
|
||||
if r.delimiter == r.comment {
|
||||
return &CommentIsDelimiterError{}
|
||||
}
|
||||
if !valid_delim(r.delimiter) {
|
||||
return &InvalidDelimiterError{}
|
||||
}
|
||||
mut need_read := true
|
||||
mut keep_raw := false
|
||||
mut line := ''
|
||||
mut fields := []string{}
|
||||
mut i := -1
|
||||
for {
|
||||
if need_read {
|
||||
l := r.read_line()!
|
||||
if l.len <= 0 {
|
||||
if keep_raw {
|
||||
line += '\n'
|
||||
}
|
||||
continue
|
||||
} else if l[0] == r.comment {
|
||||
if keep_raw {
|
||||
line += '\n' + l
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
if keep_raw {
|
||||
line += '\n'
|
||||
}
|
||||
line += l
|
||||
}
|
||||
need_read = false
|
||||
keep_raw = false
|
||||
}
|
||||
if line.len == 0 || line[0] != `"` { // not quoted
|
||||
j := line.index(r.delimiter.ascii_str()) or {
|
||||
// last
|
||||
fields << line[..line.len]
|
||||
break
|
||||
}
|
||||
i = j
|
||||
fields << line[..i]
|
||||
line = line[i + 1..]
|
||||
continue
|
||||
} else { // quoted
|
||||
mut need_more := true
|
||||
mut has_double_quotes := false
|
||||
mut j := 0
|
||||
mut n := 1
|
||||
for n < line.len {
|
||||
if line[n] == `"` {
|
||||
if n == line.len - 1 || line[n + 1] != `"` {
|
||||
need_more = false
|
||||
j = n - 1
|
||||
break
|
||||
} else {
|
||||
has_double_quotes = true
|
||||
n++
|
||||
}
|
||||
}
|
||||
n++
|
||||
}
|
||||
if need_more {
|
||||
need_read = true
|
||||
keep_raw = true
|
||||
continue
|
||||
}
|
||||
line = line[1..]
|
||||
if j + 1 == line.len {
|
||||
// last record
|
||||
fields << if has_double_quotes { line[..j].replace('""', '"') } else { line[..j] }
|
||||
break
|
||||
}
|
||||
next := line[j + 1]
|
||||
if next == r.delimiter {
|
||||
fields << if has_double_quotes { line[..j].replace('""', '"') } else { line[..j] }
|
||||
if j + 2 == line.len {
|
||||
line = ''
|
||||
} else {
|
||||
line = line[j + 2..]
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if i <= -1 && fields.len == 0 {
|
||||
return &InvalidDelimiterError{}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
fn valid_delim(b u8) bool {
|
||||
return b != 0 && b != `"` && b != `\r` && b != `\n`
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import encoding.csv
|
||||
|
||||
fn test_encoding_csv_reader() {
|
||||
data := 'name,email,phone,other\njoe,joe@blow.com,0400000000,test\nsam,sam@likesham.com,0433000000,"test quoted field"\n#chris,chris@nomail.com,94444444,"commented row"\n'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'email'
|
||||
assert row[2] == 'phone'
|
||||
assert row[3] == 'other'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'joe'
|
||||
assert row[1] == 'joe@blow.com'
|
||||
assert row[2] == '0400000000'
|
||||
assert row[3] == 'test'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == 'sam'
|
||||
assert row[1] == 'sam@likesham.com'
|
||||
assert row[2] == '0433000000'
|
||||
// quoted field
|
||||
assert row[3] == 'test quoted field'
|
||||
}
|
||||
}
|
||||
assert row_count == 3
|
||||
}
|
||||
|
||||
fn test_encoding_csv_reader_with_custom_delimiter() {
|
||||
data := 'name|email|phone|other\njoe|joe@blow.com|0400000000|test\nsam|sam@likesham.com|0433000000|"test quoted field"\n#chris|chris@nomail.com|94444444|"commented row"\n'
|
||||
mut csv_reader := csv.new_reader(data, delimiter: `|`)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'email'
|
||||
assert row[2] == 'phone'
|
||||
assert row[3] == 'other'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'joe'
|
||||
assert row[1] == 'joe@blow.com'
|
||||
assert row[2] == '0400000000'
|
||||
assert row[3] == 'test'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == 'sam'
|
||||
assert row[1] == 'sam@likesham.com'
|
||||
assert row[2] == '0433000000'
|
||||
// quoted field
|
||||
assert row[3] == 'test quoted field'
|
||||
}
|
||||
}
|
||||
assert row_count == 3
|
||||
}
|
||||
|
||||
fn test_line_break_lf() {
|
||||
lf_data := 'name,email\njoe,joe@blow.com\n'
|
||||
mut csv_reader := csv.new_reader(lf_data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'email'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'joe'
|
||||
assert row[1] == 'joe@blow.com'
|
||||
}
|
||||
}
|
||||
assert row_count == 2
|
||||
}
|
||||
|
||||
fn test_line_break_cr() {
|
||||
cr_data := 'name,email\rjoe,joe@blow.com\r'
|
||||
mut csv_reader := csv.new_reader(cr_data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'email'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'joe'
|
||||
assert row[1] == 'joe@blow.com'
|
||||
}
|
||||
}
|
||||
assert row_count == 2
|
||||
}
|
||||
|
||||
fn test_line_break_crlf() {
|
||||
crlf_data := 'name,email\r\njoe,joe@blow.com\r\n'
|
||||
mut csv_reader := csv.new_reader(crlf_data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'email'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'joe'
|
||||
assert row[1] == 'joe@blow.com'
|
||||
}
|
||||
}
|
||||
assert row_count == 2
|
||||
}
|
||||
|
||||
fn test_no_line_ending() {
|
||||
data := 'name,email,phone,other\njoe,joe@blow.com,0400000000,test'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
csv_reader.read() or { break }
|
||||
row_count++
|
||||
}
|
||||
assert row_count == 2
|
||||
}
|
||||
|
||||
fn test_last_field_empty() {
|
||||
data := '"name","description","value"\n"one","first","1"\n"two","second",\n'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'description'
|
||||
assert row[2] == 'value'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'one'
|
||||
assert row[1] == 'first'
|
||||
assert row[2] == '1'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == 'two'
|
||||
assert row[1] == 'second'
|
||||
assert row[2] == ''
|
||||
}
|
||||
}
|
||||
assert row_count == 3
|
||||
}
|
||||
|
||||
fn test_empty_fields_no_quotes() {
|
||||
data := '1,2,3,4\n,6,7,8\n9,,11,12\n13,14,,16\n17,18,19,\n'
|
||||
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == '1'
|
||||
assert row[1] == '2'
|
||||
assert row[2] == '3'
|
||||
assert row[3] == '4'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == ''
|
||||
assert row[1] == '6'
|
||||
assert row[2] == '7'
|
||||
assert row[3] == '8'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == '9'
|
||||
assert row[1] == ''
|
||||
assert row[2] == '11'
|
||||
assert row[3] == '12'
|
||||
} else if row_count == 4 {
|
||||
assert row[0] == '13'
|
||||
assert row[1] == '14'
|
||||
assert row[2] == ''
|
||||
assert row[3] == '16'
|
||||
} else if row_count == 5 {
|
||||
assert row[0] == '17'
|
||||
assert row[1] == '18'
|
||||
assert row[2] == '19'
|
||||
assert row[3] == ''
|
||||
}
|
||||
}
|
||||
assert row_count == 5
|
||||
}
|
||||
|
||||
fn test_empty_line() {
|
||||
data := '"name","description","value"\n\n\n"one","first","1"\n\n"two","second",\n'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'description'
|
||||
assert row[2] == 'value'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'one'
|
||||
assert row[1] == 'first'
|
||||
assert row[2] == '1'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == 'two'
|
||||
assert row[1] == 'second'
|
||||
}
|
||||
}
|
||||
assert row_count == 3
|
||||
}
|
||||
|
||||
fn test_field_multiple_line() {
|
||||
data := '"name","multiple
|
||||
|
||||
line","value"\n"one","first","1"\n'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'name'
|
||||
assert row[1] == 'multiple\n\n line'
|
||||
assert row[2] == 'value'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'one'
|
||||
assert row[1] == 'first'
|
||||
assert row[2] == '1'
|
||||
}
|
||||
}
|
||||
assert row_count == 2
|
||||
}
|
||||
|
||||
fn test_field_quotes_for_parts() {
|
||||
data := 'a1,"b1",c1\n"a2",b2,c2\na3,b3,"c3"\na4,b4,c4\n'
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == 'a1'
|
||||
assert row[1] == 'b1'
|
||||
assert row[2] == 'c1'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == 'a2'
|
||||
assert row[1] == 'b2'
|
||||
assert row[2] == 'c2'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == 'a3'
|
||||
assert row[1] == 'b3'
|
||||
assert row[2] == 'c3'
|
||||
} else if row_count == 4 {
|
||||
assert row[0] == 'a4'
|
||||
assert row[1] == 'b4'
|
||||
assert row[2] == 'c4'
|
||||
}
|
||||
}
|
||||
assert row_count == 4
|
||||
}
|
||||
|
||||
fn test_field_double_quotes() {
|
||||
row1 := '11,"12\n13"\n'
|
||||
row2 := '21,"2""2""\n23"\n'
|
||||
row3 := '"3""1""",32\n'
|
||||
data := row1 + row2 + row3
|
||||
mut csv_reader := csv.new_reader(data)
|
||||
mut row_count := 0
|
||||
for {
|
||||
row := csv_reader.read() or { break }
|
||||
row_count++
|
||||
if row_count == 1 {
|
||||
assert row[0] == '11'
|
||||
assert row[1] == '12\n13'
|
||||
} else if row_count == 2 {
|
||||
assert row[0] == '21'
|
||||
assert row[1] == '2"2"\n23'
|
||||
} else if row_count == 3 {
|
||||
assert row[0] == '3"1"'
|
||||
assert row[1] == '32'
|
||||
}
|
||||
}
|
||||
assert row_count == 3
|
||||
}
|
||||
|
||||
struct Test {
|
||||
mut:
|
||||
id int
|
||||
bonus string
|
||||
amount int
|
||||
yes bool
|
||||
}
|
||||
|
||||
fn test_decode_to_struct() {
|
||||
text := 'id,bonus,amount,yes\r\n1,bomb,1,true\r\n2,rocket,1,false,\r\n3,lightning,2,2\r\n'
|
||||
arr := csv.decode[Test](text)
|
||||
assert arr[0].id == 1
|
||||
assert arr[0].bonus == 'bomb'
|
||||
assert arr[0].amount == 1
|
||||
assert arr[0].yes == true
|
||||
assert arr[1].id == 2
|
||||
assert arr[1].bonus == 'rocket'
|
||||
assert arr[1].amount == 1
|
||||
assert arr[1].yes == false
|
||||
assert arr[2].id == 3
|
||||
assert arr[2].bonus == 'lightning'
|
||||
assert arr[2].amount == 2
|
||||
assert arr[2].yes == true
|
||||
assert arr.len == 3
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
module csv
|
||||
|
||||
import strconv
|
||||
|
||||
// decode csv to struct
|
||||
pub fn decode[T](data string) []T {
|
||||
mut result := []T{}
|
||||
if data == '' {
|
||||
return result
|
||||
}
|
||||
|
||||
mut parser := new_reader(data)
|
||||
mut columns_names := []string{}
|
||||
mut i := 0
|
||||
for {
|
||||
items := parser.read() or { break }
|
||||
if i == 0 {
|
||||
for val in items {
|
||||
columns_names << val
|
||||
}
|
||||
} else {
|
||||
mut t_val := T{}
|
||||
$for field in T.fields {
|
||||
col := get_column(field.name, columns_names)
|
||||
if col > -1 && col < items.len {
|
||||
$if field.typ is string {
|
||||
t_val.$(field.name) = items[col]
|
||||
} $else $if field.typ is int {
|
||||
t_val.$(field.name) = items[col].int()
|
||||
} $else $if field.typ is f32 {
|
||||
t_val.$(field.name) = f32(strconv.atof64(items[col]) or { f32(0.0) })
|
||||
} $else $if field.typ is f64 {
|
||||
t_val.$(field.name) = strconv.atof64(items[col]) or { f64(0.0) }
|
||||
} $else $if field.typ is bool {
|
||||
t_val.$(field.name) = string_to_bool(items[col])
|
||||
}
|
||||
}
|
||||
}
|
||||
result << t_val
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fn string_to_bool(val string) bool {
|
||||
l_val := val.to_lower().trim_space()
|
||||
if l_val == 'true' {
|
||||
return true
|
||||
}
|
||||
|
||||
i_val := val.int()
|
||||
if i_val != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fn get_column(name string, columns []string) int {
|
||||
for i, val in columns {
|
||||
if val == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module csv
|
||||
|
||||
import os
|
||||
|
||||
// new_reader_from_file create a csv reader from a file
|
||||
pub fn new_reader_from_file(csv_file_path string, config ReaderConfig) !&Reader {
|
||||
csv_file_content := os.read_file(csv_file_path)!
|
||||
return new_reader(csv_file_content, config)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module csv
|
||||
|
||||
import os
|
||||
|
||||
fn test_new_reader_from_file() {
|
||||
test_file_path_for_reader := os.join_path(os.temp_dir(), 'test_new_reader_from_file.csv')
|
||||
|
||||
text := 'id,bonus,amount,yes\n1,bomb,1,true\n2,rocket,1,false,\n3,lightning,2,2\n'
|
||||
os.write_file(test_file_path_for_reader, text)!
|
||||
|
||||
mut reader := new_reader_from_file(test_file_path_for_reader)!
|
||||
mut writer := new_writer()
|
||||
|
||||
for {
|
||||
row := reader.read() or { break }
|
||||
writer.write(row) or { panic(err) }
|
||||
}
|
||||
|
||||
assert text == writer.str()
|
||||
|
||||
os.rm(test_file_path_for_reader)!
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module csv
|
||||
|
||||
import strings
|
||||
|
||||
struct Writer {
|
||||
use_crlf bool
|
||||
delimiter u8
|
||||
mut:
|
||||
sb strings.Builder
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct WriterConfig {
|
||||
pub:
|
||||
use_crlf bool
|
||||
delimiter u8 = `,`
|
||||
}
|
||||
|
||||
// new_writer returns a reference to a Writer
|
||||
pub fn new_writer(config WriterConfig) &Writer {
|
||||
return &Writer{
|
||||
sb: strings.new_builder(200)
|
||||
use_crlf: config.use_crlf
|
||||
delimiter: config.delimiter
|
||||
}
|
||||
}
|
||||
|
||||
// write writes a single record
|
||||
pub fn (mut w Writer) write(record []string) !bool {
|
||||
if !valid_delim(w.delimiter) {
|
||||
return &InvalidDelimiterError{}
|
||||
}
|
||||
le := if w.use_crlf { '\r\n' } else { '\n' }
|
||||
for n, field_ in record {
|
||||
mut field := field_
|
||||
if n > 0 {
|
||||
w.sb.write_string(w.delimiter.ascii_str())
|
||||
}
|
||||
if !w.field_needs_quotes(field) {
|
||||
w.sb.write_string(field)
|
||||
continue
|
||||
}
|
||||
w.sb.write_string('"')
|
||||
for field.len > 0 {
|
||||
mut i := field.index_any('"\r\n')
|
||||
if i < 0 {
|
||||
i = field.len
|
||||
}
|
||||
w.sb.write_string(field[..i])
|
||||
field = field[i..]
|
||||
if field.len > 0 {
|
||||
z := field[0]
|
||||
match z {
|
||||
`"` { w.sb.write_string('""') }
|
||||
`\r`, `\n` { w.sb.write_string(le) }
|
||||
else {}
|
||||
}
|
||||
|
||||
field = field[1..]
|
||||
}
|
||||
}
|
||||
w.sb.write_string('"')
|
||||
}
|
||||
w.sb.write_string(le)
|
||||
return true
|
||||
}
|
||||
|
||||
// Once we have multi dimensional array
|
||||
// pub fn (w &Writer) write_all(records [][]string) {
|
||||
// for _, record in records {
|
||||
// w.write(record)
|
||||
// }
|
||||
// }
|
||||
fn (w &Writer) field_needs_quotes(field string) bool {
|
||||
if field == '' {
|
||||
return false
|
||||
}
|
||||
if field.contains(w.delimiter.ascii_str()) || field.index_any('"\r\n') != -1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// str returns the writer contents
|
||||
pub fn (mut w Writer) str() string {
|
||||
return w.sb.str()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import encoding.csv
|
||||
|
||||
fn test_encoding_csv_writer() {
|
||||
mut csv_writer := csv.new_writer()
|
||||
|
||||
csv_writer.write(['name', 'email', 'phone', 'other']) or {}
|
||||
csv_writer.write(['joe', 'joe@blow.com', '0400000000', 'test']) or {}
|
||||
csv_writer.write(['sam', 'sam@likesham.com', '0433000000', 'needs, quoting']) or {}
|
||||
|
||||
assert csv_writer.str() == 'name,email,phone,other\njoe,joe@blow.com,0400000000,test\nsam,sam@likesham.com,0433000000,"needs, quoting"\n'
|
||||
|
||||
/*
|
||||
mut csv_writer2 := csv.new_writer(delimiter:':')
|
||||
csv_writer.write(['foo', 'bar', '2']) or {}
|
||||
assert csv_writer.str() == 'foo:bar:2'
|
||||
*/
|
||||
}
|
||||
|
||||
fn test_encoding_csv_writer_delimiter() {
|
||||
mut csv_writer := csv.new_writer(delimiter: ` `)
|
||||
|
||||
csv_writer.write(['name', 'email', 'phone', 'other']) or {}
|
||||
csv_writer.write(['joe', 'joe@blow.com', '0400000000', 'test']) or {}
|
||||
csv_writer.write(['sam', 'sam@likesham.com', '0433000000', 'needs, quoting']) or {}
|
||||
|
||||
assert csv_writer.str() == 'name email phone other\njoe joe@blow.com 0400000000 test\nsam sam@likesham.com 0433000000 "needs, quoting"\n'
|
||||
}
|
||||
Reference in new issue
Block a user