Copy/split csv-data-manipulation as a lib.
This commit is contained in:
parent
e7c1b7d973
commit
ab8879aab7
5 changed files with 270 additions and 244 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
bin/*
|
bin/*
|
||||||
tmp/*
|
tmp/*
|
||||||
|
deps/
|
||||||
|
*.o
|
||||||
|
|
5
Makefile
5
Makefile
|
@ -1,11 +1,12 @@
|
||||||
|
|
||||||
SRC = $(wildcard src/*.c)
|
SRC = $(wildcard src/*.c)
|
||||||
DEPS = $(wildcard deps/**/*.c)
|
DEPS = $(wildcard deps/**/*.c)
|
||||||
OBJS = $(patsubst %.c,%.o,$(DEPS))
|
LIBS = $(wildcard lib/**/*.c)
|
||||||
|
OBJS = $(patsubst %.c,%.o,$(DEPS) $(LIBS))
|
||||||
BINS = $(patsubst src/%,bin/%,$(patsubst %.c,%,$(SRC)))
|
BINS = $(patsubst src/%,bin/%,$(patsubst %.c,%,$(SRC)))
|
||||||
|
|
||||||
# CFLAGS = -std=c99 -Ideps -Wall -Wno-unused-function -U__STRICT_ANSI__
|
# CFLAGS = -std=c99 -Ideps -Wall -Wno-unused-function -U__STRICT_ANSI__
|
||||||
CFLAGS = -ggdb -std=c99 -Ideps -Wall -Wno-unused-function -pedantic
|
CFLAGS = -ggdb -std=c99 -Ideps -Ilib -Wall -Wno-unused-function -pedantic
|
||||||
|
|
||||||
|
|
||||||
all: $(BINS)
|
all: $(BINS)
|
||||||
|
|
233
lib/libcsv/libcsv.c
Normal file
233
lib/libcsv/libcsv.c
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
|
||||||
|
#include "libcsv.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How to read a CSV file ?
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to trim whitespaces from left & right of a string
|
||||||
|
*/
|
||||||
|
int trim(char ** str) {
|
||||||
|
int trimmed;
|
||||||
|
int n;
|
||||||
|
int len;
|
||||||
|
|
||||||
|
len = strlen(*str);
|
||||||
|
n = len - 1;
|
||||||
|
/* from right */
|
||||||
|
while((n>=0) && isspace((*str)[n])) {
|
||||||
|
(*str)[n] = '\0';
|
||||||
|
trimmed += 1;
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* from left */
|
||||||
|
n = 0;
|
||||||
|
while((n < len) && (isspace((*str)[0]))) {
|
||||||
|
(*str)[0] = '\0';
|
||||||
|
*str = (*str)+1;
|
||||||
|
trimmed += 1;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-allocate csv structure
|
||||||
|
*/
|
||||||
|
int csv_destroy(CSV * csv) {
|
||||||
|
if (csv == NULL) { return 0; }
|
||||||
|
if (csv->table != NULL) { free(csv->table); }
|
||||||
|
if (csv->delim != NULL) { free(csv->delim); }
|
||||||
|
free(csv);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate memory for a CSV structure
|
||||||
|
*/
|
||||||
|
CSV * csv_create(unsigned int cols, unsigned int rows) {
|
||||||
|
CSV * csv;
|
||||||
|
|
||||||
|
csv = malloc(sizeof(CSV));
|
||||||
|
csv->rows = rows;
|
||||||
|
csv->cols = cols;
|
||||||
|
csv->delim = strdup(",");
|
||||||
|
|
||||||
|
csv->table = malloc(sizeof(char *) * cols * rows);
|
||||||
|
if (csv->table == NULL) { goto error; }
|
||||||
|
|
||||||
|
memset(csv->table, 0, sizeof(char *) * cols * rows);
|
||||||
|
|
||||||
|
return csv;
|
||||||
|
|
||||||
|
error:
|
||||||
|
csv_destroy(csv);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value in CSV table at COL, ROW
|
||||||
|
*/
|
||||||
|
char * csv_get(CSV * csv, unsigned int col, unsigned int row) {
|
||||||
|
unsigned int idx;
|
||||||
|
idx = col + (row * csv->cols);
|
||||||
|
return csv->table[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set value in CSV table at COL, ROW
|
||||||
|
*/
|
||||||
|
int csv_set(CSV * csv, unsigned int col, unsigned int row, char * value) {
|
||||||
|
unsigned int idx;
|
||||||
|
idx = col + (row * csv->cols);
|
||||||
|
csv->table[idx] = value;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void csv_display(CSV * csv) {
|
||||||
|
int row, col;
|
||||||
|
char * content;
|
||||||
|
if ((csv->rows == 0) || (csv->cols==0)) {
|
||||||
|
printf("[Empty table]\n");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n[Table cols=%d rows=%d]\n", csv->cols, csv->rows);
|
||||||
|
for (row=0; row<csv->rows; row++) {
|
||||||
|
printf("[|");
|
||||||
|
for (col=0; col<csv->cols; col++) {
|
||||||
|
content = csv_get(csv, col, row);
|
||||||
|
printf("%s\t|", content);
|
||||||
|
}
|
||||||
|
printf("]\n");
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize CSV table
|
||||||
|
*/
|
||||||
|
int csv_resize(CSV * old_csv, unsigned int new_cols, unsigned int new_rows) {
|
||||||
|
unsigned int cur_col,
|
||||||
|
cur_row,
|
||||||
|
max_cols,
|
||||||
|
max_rows;
|
||||||
|
CSV * new_csv;
|
||||||
|
char * content;
|
||||||
|
bool in_old, in_new;
|
||||||
|
|
||||||
|
/* Build a new (fake) csv */
|
||||||
|
new_csv = csv_create(new_cols, new_rows);
|
||||||
|
if (new_csv == NULL) { goto error; }
|
||||||
|
|
||||||
|
new_csv->rows = new_rows;
|
||||||
|
new_csv->cols = new_cols;
|
||||||
|
|
||||||
|
|
||||||
|
max_cols = (new_cols > old_csv->cols)? new_cols : old_csv->cols;
|
||||||
|
max_rows = (new_rows > old_csv->rows)? new_rows : old_csv->rows;
|
||||||
|
|
||||||
|
for (cur_col=0; cur_col<max_cols; cur_col++) {
|
||||||
|
for (cur_row=0; cur_row<max_rows; cur_row++) {
|
||||||
|
in_old = (cur_col < old_csv->cols) && (cur_row < old_csv->rows);
|
||||||
|
in_new = (cur_col < new_csv->cols) && (cur_row < new_csv->rows);
|
||||||
|
|
||||||
|
if (in_old && in_new) {
|
||||||
|
/* re-link data */
|
||||||
|
content = csv_get(old_csv, cur_col, cur_row);
|
||||||
|
csv_set(new_csv, cur_col, cur_row, content);
|
||||||
|
} else if (in_old) {
|
||||||
|
/* destroy data */
|
||||||
|
content = csv_get(old_csv, cur_col, cur_row);
|
||||||
|
free(content);
|
||||||
|
} else { /* skip */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* on rows */
|
||||||
|
free(old_csv->table);
|
||||||
|
old_csv->rows = new_rows;
|
||||||
|
old_csv->cols = new_cols;
|
||||||
|
old_csv->table = new_csv->table;
|
||||||
|
new_csv->table = NULL;
|
||||||
|
csv_destroy(new_csv);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
error:
|
||||||
|
printf("Unable to resize CSV table: error %d - %s\n", errno, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open CSV file and load its content into provided CSV structure
|
||||||
|
**/
|
||||||
|
int csv_open(CSV * csv, char * filename) {
|
||||||
|
FILE * fp;
|
||||||
|
unsigned int m_rows;
|
||||||
|
unsigned int m_cols, cols;
|
||||||
|
char line[2048];
|
||||||
|
char * lineptr;
|
||||||
|
char * token;
|
||||||
|
|
||||||
|
|
||||||
|
fp = fopen(filename, "r");
|
||||||
|
if (fp == NULL) { goto error; }
|
||||||
|
|
||||||
|
m_rows = 0;
|
||||||
|
m_cols = 0;
|
||||||
|
while(fgets(line, sizeof(line), fp) != NULL) {
|
||||||
|
m_rows += 1;
|
||||||
|
cols = 0;
|
||||||
|
lineptr = line;
|
||||||
|
while ((token = strtok(lineptr, csv->delim)) != NULL) {
|
||||||
|
lineptr = NULL;
|
||||||
|
trim(&token);
|
||||||
|
cols += 1;
|
||||||
|
if (cols > m_cols) { m_cols = cols; }
|
||||||
|
csv_resize(csv, m_cols, m_rows);
|
||||||
|
csv_set(csv, cols-1, m_rows-1, strdup(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
csv->rows = m_rows;
|
||||||
|
csv->cols = m_cols;
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
error:
|
||||||
|
fclose(fp);
|
||||||
|
printf("Unable to open %s for reading.", filename);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open CSV file and save CSV structure content into it
|
||||||
|
**/
|
||||||
|
int csv_save(CSV * csv, char * filename) {
|
||||||
|
FILE * fp;
|
||||||
|
int row, col;
|
||||||
|
char * content;
|
||||||
|
|
||||||
|
fp = fopen(filename, "w");
|
||||||
|
for (row=0; row<csv->rows; row++) {
|
||||||
|
for (col=0; col<csv->cols; col++) {
|
||||||
|
content = csv_get(csv, col, row);
|
||||||
|
fprintf(fp, "%s%s", content,
|
||||||
|
((col == csv->cols-1) ? "" : csv->delim) );
|
||||||
|
}
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
28
lib/libcsv/libcsv.h
Normal file
28
lib/libcsv/libcsv.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#define bool int
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h> /* malloc...*/
|
||||||
|
#include <string.h> /* strtok...*/
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char * delim;
|
||||||
|
unsigned int rows;
|
||||||
|
unsigned int cols;
|
||||||
|
char ** table;
|
||||||
|
} CSV;
|
||||||
|
|
||||||
|
|
||||||
|
/* libcsv.c */
|
||||||
|
int trim(char **str);
|
||||||
|
int csv_destroy(CSV *csv);
|
||||||
|
CSV *csv_create(unsigned int cols, unsigned int rows);
|
||||||
|
char *csv_get(CSV *csv, unsigned int col, unsigned int row);
|
||||||
|
int csv_set(CSV *csv, unsigned int col, unsigned int row, char *value);
|
||||||
|
void csv_display(CSV *csv);
|
||||||
|
int csv_resize(CSV *old_csv, unsigned int new_cols, unsigned int new_rows);
|
||||||
|
int csv_open(CSV *csv, char *filename);
|
||||||
|
int csv_save(CSV *csv, char *filename);
|
|
@ -2,250 +2,11 @@
|
||||||
#define TITLE "CSV data manipulation"
|
#define TITLE "CSV data manipulation"
|
||||||
#define URL "http://rosettacode.org/wiki/CSV_data_manipulation"
|
#define URL "http://rosettacode.org/wiki/CSV_data_manipulation"
|
||||||
|
|
||||||
#define _GNU_SOURCE
|
#include <libcsv/libcsv.h>
|
||||||
#define bool int
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h> /* malloc...*/
|
|
||||||
#include <string.h> /* strtok...*/
|
|
||||||
#include <ctype.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How to read a CSV file ?
|
* Test
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char * delim;
|
|
||||||
unsigned int rows;
|
|
||||||
unsigned int cols;
|
|
||||||
char ** table;
|
|
||||||
} CSV;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to trim whitespaces from left & right of a string
|
|
||||||
*/
|
|
||||||
int trim(char ** str) {
|
|
||||||
int trimmed;
|
|
||||||
int n;
|
|
||||||
int len;
|
|
||||||
|
|
||||||
len = strlen(*str);
|
|
||||||
n = len - 1;
|
|
||||||
/* from right */
|
|
||||||
while((n>=0) && isspace((*str)[n])) {
|
|
||||||
(*str)[n] = '\0';
|
|
||||||
trimmed += 1;
|
|
||||||
n--;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* from left */
|
|
||||||
n = 0;
|
|
||||||
while((n < len) && (isspace((*str)[0]))) {
|
|
||||||
(*str)[0] = '\0';
|
|
||||||
*str = (*str)+1;
|
|
||||||
trimmed += 1;
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-allocate csv structure
|
|
||||||
*/
|
|
||||||
int csv_destroy(CSV * csv) {
|
|
||||||
if (csv == NULL) { return 0; }
|
|
||||||
if (csv->table != NULL) { free(csv->table); }
|
|
||||||
if (csv->delim != NULL) { free(csv->delim); }
|
|
||||||
free(csv);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocate memory for a CSV structure
|
|
||||||
*/
|
|
||||||
CSV * csv_create(unsigned int cols, unsigned int rows) {
|
|
||||||
CSV * csv;
|
|
||||||
|
|
||||||
csv = malloc(sizeof(CSV));
|
|
||||||
csv->rows = rows;
|
|
||||||
csv->cols = cols;
|
|
||||||
csv->delim = strdup(",");
|
|
||||||
|
|
||||||
csv->table = malloc(sizeof(char *) * cols * rows);
|
|
||||||
if (csv->table == NULL) { goto error; }
|
|
||||||
|
|
||||||
memset(csv->table, 0, sizeof(char *) * cols * rows);
|
|
||||||
|
|
||||||
return csv;
|
|
||||||
|
|
||||||
error:
|
|
||||||
csv_destroy(csv);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get value in CSV table at COL, ROW
|
|
||||||
*/
|
|
||||||
char * csv_get(CSV * csv, unsigned int col, unsigned int row) {
|
|
||||||
unsigned int idx;
|
|
||||||
idx = col + (row * csv->cols);
|
|
||||||
return csv->table[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set value in CSV table at COL, ROW
|
|
||||||
*/
|
|
||||||
int csv_set(CSV * csv, unsigned int col, unsigned int row, char * value) {
|
|
||||||
unsigned int idx;
|
|
||||||
idx = col + (row * csv->cols);
|
|
||||||
csv->table[idx] = value;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void csv_display(CSV * csv) {
|
|
||||||
int row, col;
|
|
||||||
char * content;
|
|
||||||
if ((csv->rows == 0) || (csv->cols==0)) {
|
|
||||||
printf("[Empty table]\n");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("\n[Table cols=%d rows=%d]\n", csv->cols, csv->rows);
|
|
||||||
for (row=0; row<csv->rows; row++) {
|
|
||||||
printf("[|");
|
|
||||||
for (col=0; col<csv->cols; col++) {
|
|
||||||
content = csv_get(csv, col, row);
|
|
||||||
printf("%s\t|", content);
|
|
||||||
}
|
|
||||||
printf("]\n");
|
|
||||||
}
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resize CSV table
|
|
||||||
* - grow columns: on each row, add missing columns cells
|
|
||||||
* - grow rows: add now rows, with all columns count
|
|
||||||
* - reduce columns: remove columns from right
|
|
||||||
* - reduce lines: remove columns from the end
|
|
||||||
*/
|
|
||||||
int csv_resize(CSV * old_csv, unsigned int new_cols, unsigned int new_rows) {
|
|
||||||
unsigned int cur_col,
|
|
||||||
cur_row,
|
|
||||||
max_cols,
|
|
||||||
max_rows;
|
|
||||||
CSV * new_csv;
|
|
||||||
char * content;
|
|
||||||
bool in_old, in_new;
|
|
||||||
|
|
||||||
/* Build a new (fake) csv */
|
|
||||||
new_csv = csv_create(new_cols, new_rows);
|
|
||||||
if (new_csv == NULL) { goto error; }
|
|
||||||
|
|
||||||
new_csv->rows = new_rows;
|
|
||||||
new_csv->cols = new_cols;
|
|
||||||
|
|
||||||
|
|
||||||
max_cols = (new_cols > old_csv->cols)? new_cols : old_csv->cols;
|
|
||||||
max_rows = (new_rows > old_csv->rows)? new_rows : old_csv->rows;
|
|
||||||
|
|
||||||
for (cur_col=0; cur_col<max_cols; cur_col++) {
|
|
||||||
for (cur_row=0; cur_row<max_rows; cur_row++) {
|
|
||||||
in_old = (cur_col < old_csv->cols) && (cur_row < old_csv->rows);
|
|
||||||
in_new = (cur_col < new_csv->cols) && (cur_row < new_csv->rows);
|
|
||||||
|
|
||||||
if (in_old && in_new) {
|
|
||||||
/* re-link data */
|
|
||||||
content = csv_get(old_csv, cur_col, cur_row);
|
|
||||||
csv_set(new_csv, cur_col, cur_row, content);
|
|
||||||
} else if (in_old) {
|
|
||||||
/* destroy data */
|
|
||||||
content = csv_get(old_csv, cur_col, cur_row);
|
|
||||||
free(content);
|
|
||||||
} else { /* skip */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* on rows */
|
|
||||||
free(old_csv->table);
|
|
||||||
old_csv->rows = new_rows;
|
|
||||||
old_csv->cols = new_cols;
|
|
||||||
old_csv->table = new_csv->table;
|
|
||||||
new_csv->table = NULL;
|
|
||||||
csv_destroy(new_csv);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
error:
|
|
||||||
printf("Unable to resize CSV table: error %d - %s\n", errno, strerror(errno));
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* , char delim='\t' */
|
|
||||||
int csv_open(CSV * csv, char * filename) {
|
|
||||||
FILE * fp;
|
|
||||||
unsigned int m_rows;
|
|
||||||
unsigned int m_cols, cols;
|
|
||||||
char line[2048];
|
|
||||||
char * lineptr;
|
|
||||||
char * token;
|
|
||||||
|
|
||||||
|
|
||||||
fp = fopen(filename, "r");
|
|
||||||
if (fp == NULL) { goto error; }
|
|
||||||
|
|
||||||
m_rows = 0;
|
|
||||||
m_cols = 0;
|
|
||||||
while(fgets(line, sizeof(line), fp) != NULL) {
|
|
||||||
m_rows += 1;
|
|
||||||
cols = 0;
|
|
||||||
lineptr = line;
|
|
||||||
while ((token = strtok(lineptr, csv->delim)) != NULL) {
|
|
||||||
lineptr = NULL;
|
|
||||||
trim(&token);
|
|
||||||
cols += 1;
|
|
||||||
if (cols > m_cols) { m_cols = cols; }
|
|
||||||
csv_resize(csv, m_cols, m_rows);
|
|
||||||
csv_set(csv, cols-1, m_rows-1, strdup(token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
csv->rows = m_rows;
|
|
||||||
csv->cols = m_cols;
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
error:
|
|
||||||
fclose(fp);
|
|
||||||
printf("Unable to open %s for reading.", filename);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int csv_save(CSV * csv, char * filename) {
|
|
||||||
FILE * fp;
|
|
||||||
int row, col;
|
|
||||||
char * content;
|
|
||||||
|
|
||||||
fp = fopen(filename, "w");
|
|
||||||
for (row=0; row<csv->rows; row++) {
|
|
||||||
for (col=0; col<csv->cols; col++) {
|
|
||||||
content = csv_get(csv, col, row);
|
|
||||||
fprintf(fp, "%s%s", content,
|
|
||||||
((col == csv->cols-1) ? "" : csv->delim) );
|
|
||||||
}
|
|
||||||
fprintf(fp, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char ** argv) {
|
int main(int argc, char ** argv) {
|
||||||
CSV * csv;
|
CSV * csv;
|
||||||
|
|
||||||
|
@ -262,8 +23,9 @@ int main(int argc, char ** argv) {
|
||||||
csv_set(csv, 4, 4, "400");
|
csv_set(csv, 4, 4, "400");
|
||||||
csv_display(csv);
|
csv_display(csv);
|
||||||
|
|
||||||
csv_save(csv, "tmp/csv-data-manupulation.out.csv");
|
csv_save(csv, "tmp/csv-data-manipulation.result.csv");
|
||||||
csv_destroy(csv);
|
csv_destroy(csv);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue