Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+19
View File
@@ -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
}
+473
View File
@@ -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)!
}
+216
View File
@@ -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`
}
+307
View File
@@ -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
}
+68
View File
@@ -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
}
+12
View File
@@ -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)
}
+25
View File
@@ -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)!
}
+90
View File
@@ -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()
}
+27
View File
@@ -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'
}