cards

alternative to Anki for the command-line
git clone https://github.com/jennydoe/cards.git
Log | Files | Refs | Feed | README

commit 30fba31f26ce666667e6a091e93f164400955d1b
parent 03acec26db9b6a807efaf806fa4addc4232c987a
Author: Jenny Doe <tng@soykaf.me>
Date:   Fri,  5 Apr 2019 22:39:05 +0200

added: csv support

Diffstat:
MMakefile | 2+-
Mcards.c | 195++++++++++++++++++++++++++++++++++++++-----------------------------------------
Acards.csv | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acsv.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acsv.h | 12++++++++++++
Afread_csv_line.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mreadme | 5+++++
Asplit.c | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 537 insertions(+), 103 deletions(-)

diff --git a/Makefile b/Makefile @@ -2,7 +2,7 @@ CC=cc STD=c99 CFLAGS=-g -Wall BIN=a.out -SRC=cards.c +SRC=cards.c csv.c split.c fread_csv_line.c $(BIN): $(SRC) $(CC) --std=$(STD) $(CFLAGS) -o $(BIN) $(SRC) diff --git a/cards.c b/cards.c @@ -1,5 +1,5 @@ #include <stdio.h> -#include <stdlib.h> /* srand(), rand() */ +#include <stdlib.h> /* srand(), rand() */ #include <string.h> /* sleep(), getopt() */ @@ -12,6 +12,13 @@ #include <locale.h> #include <wchar.h> +#include "csv.h" + +#define MAX_CSV_SIZE 1024 +#define MAX_QA_SIZE 512 + +#define CARDS "cards.csv" + struct Card { /* a single card (a bucket's node) */ wchar_t *question; /* key */ @@ -126,7 +133,7 @@ hash_table_iterate_over(struct Cards *ht, void (*f) (struct Card *)) } /* accessed in main() and select_next_card() */ -static struct Card * next_card; +static struct Card *next_card; void select_next_card(struct Card *card) @@ -134,10 +141,10 @@ select_next_card(struct Card *card) if (next_card == NULL || (card->priority > next_card->priority - && card->last_appearance <= next_card->last_appearance)) { - if (rand() >= RAND_MAX / 3) /* 2/3 chances of being + && card->last_appearance <= next_card->last_appearance + && (rand() >= RAND_MAX / 3))) { /* 2/3 chances of being * selected */ - next_card = card; + next_card = card; } return; @@ -151,6 +158,48 @@ dump_card(struct Card *card) return; } +void +dump_csv(struct Card *card) +{ + FILE *fp = fopen(CARDS, "a"); + if (fp == NULL) + fprintf(stderr, "Could not open file!\n"); + + + wchar_t *dup = wcsdup(card->question); + if (dup == NULL) + return; + wchar_t c; + int i = 0; + while ((c = dup[i]) != '\0') { + if (dup[i] != '\n') { + fprintf(fp, "%lc", dup[i]); + } + ++i; + } + free(dup); + + fputws(L",", fp); + + dup = wcsdup(card->answer); + if (dup == NULL) + return; + i = 0; + while ((c = dup[i]) != '\0') { + if (dup[i] != L'\n') { + fprintf(fp, "%lc", dup[i]); + } + ++i; + } + free(dup); + + fprintf(fp, ",%u\n", card->priority); + + fclose(fp); + + return; +} + #define ht_init hash_table_init #define ht_insert hash_table_insert #define ht_destroy hash_table_destroy @@ -170,7 +219,7 @@ main(int argc, char *argv[]) exit(1); } - wchar_t *line = malloc(sizeof(wchar_t) * 1024); + wchar_t *line = malloc(sizeof(wchar_t) * MAX_QA_SIZE); if (line == NULL) { ht_destroy(cards); free(cards); @@ -178,110 +227,40 @@ main(int argc, char *argv[]) exit(1); } - /* int z = 1000; */ - /* and z-- instead of 1000 */ - -#define o(k,v) \ - ht_insert(cards, k, v, 1000, 0) /* or 1 */ + FILE *fp = fopen(CARDS, "r"); + if (fp == NULL) + fprintf(stderr, "Could not open file!\n"); - o(L"あ", L"a\n"); - o(L"い", L"i\n"); - o(L"う", L"u\n"); - o(L"え", L"e\n"); - o(L"お", L"o\n"); - o(L"か", L"ka\n"); - o(L"き", L"ki\n"); - o(L"く", L"ku\n"); - o(L"け", L"ke\n"); - o(L"こ", L"ko\n"); + char csv_line[MAX_CSV_SIZE]; - o(L"が", L"ga\n"); - o(L"ぎ", L"gi\n"); - o(L"ぐ", L"gu\n"); - o(L"げ", L"ge\n"); - o(L"ご", L"go\n"); + while (fgets(csv_line, MAX_CSV_SIZE, fp) != NULL) { - o(L"さ", L"sa\n"); - o(L"す", L"su\n"); - o(L"せ", L"se\n"); - o(L"そ", L"so\n"); +#define o(k,v, p, l) \ + ht_insert(cards, k, v, p, l) - o(L"ざ", L"za\n"); - o(L"ず", L"zu\n"); - o(L"ぜ", L"ze\n"); - o(L"ぞ", L"zo\n"); + char **items = parse_csv(csv_line); + if (!items[0] || !items[1] || !items[2]) + continue; - o(L"じゃ", L"jya\n"); - o(L"じゅ", L"jyu\n"); - o(L"じょ", L"jyo\n"); + wchar_t q[MAX_QA_SIZE], a[MAX_QA_SIZE]; + swprintf(q, MAX_QA_SIZE, L"%hs", items[0]); + swprintf(a, MAX_QA_SIZE, L"%hs\n", items[1]); - o(L"た", L"ta\n"); - o(L"て", L"te\n"); - o(L"と", L"to\n"); + unsigned int p = strtol(items[2], NULL, 10); + o(q, a, p == 0 ? 1000 : p, 0); - o(L"だ", L"da\n"); - o(L"で", L"de\n"); - o(L"ど", L"do\n"); - - o(L"な", L"na\n"); - o(L"に", L"ni\n"); - o(L"ぬ", L"nu\n"); - o(L"ね", L"ne\n"); - o(L"の", L"no\n"); - - o(L"は", L"ha\n"); - o(L"ひ", L"hi\n"); - o(L"へ", L"he\n"); - o(L"ほ", L"ho\n"); - - o(L"ば", L"ba\n"); - o(L"び", L"bi\n"); - o(L"ぶ", L"bu\n"); - o(L"べ", L"be\n"); - o(L"ぼ", L"bo\n"); - - o(L"ぱ", L"pa\n"); - o(L"ぴ", L"pi\n"); - o(L"ぷ", L"pu\n"); - o(L"ぺ", L"pe\n"); - o(L"ぽ", L"po\n"); - - o(L"ふ", L"fu\n"); - - o(L"ま", L"ma\n"); - o(L"み", L"mi\n"); - o(L"む", L"mu\n"); - o(L"め", L"me\n"); - o(L"も", L"mo\n"); - - o(L"や", L"ya\n"); - o(L"ゆ", L"yu\n"); - o(L"よ", L"yo\n"); - - o(L"ら", L"ra\n"); - o(L"り", L"ri\n"); - o(L"る", L"ru\n"); - o(L"れ", L"re\n"); - o(L"ろ", L"ro\n"); - - o(L"わ", L"wa\n"); - o(L"を", L"wo\n"); - - o(L"ん", L"n\n"); - - o(L"つ", L"tsu\n"); - o(L"づ", L"zu\n"); - - o(L"し", L"shi\n"); - o(L"じ", L"ji\n"); - - o(L"ゔ", L"vu\n"); + free_csv_line(items); +#undef o - /* o(L"", L"\n"); */ + } -#undef o + fclose(fp); + if (cards->population == 0) { + printf("No cards!\n"); + goto end; + } struct { unsigned int show_answer; @@ -299,6 +278,7 @@ main(int argc, char *argv[]) puts("front\tback"); puts("-----\t----"); ht_iterate(cards, dump_card); + puts(""); goto end; break; case 'h': @@ -318,10 +298,19 @@ main(int argc, char *argv[]) printf("\t%ls\n", next_card->question); printf("? "); - while (fgetws(line, 1024, stdin) != NULL) { + while (fgetws(line, MAX_QA_SIZE, stdin) != NULL) { printf("\e[1;1H\e[2J"); /* clear the screen */ - if (wcscmp(next_card->answer, line) == 0) { + if (wcscmp(L"!save\n", line) == 0) { + fp = fopen(CARDS, "w"); + if (fp == NULL) + fprintf(stderr, "Could not open file!\n"); + fputs("", fp); + fclose(fp); + ht_iterate(cards, dump_csv); + i--; /* cancels i++ */ + goto prompt; /* show same card again */ + } else if (wcscmp(next_card->answer, line) == 0) { printf("Nice!\n"); next_card->priority--; success++; @@ -330,12 +319,14 @@ main(int argc, char *argv[]) next_card->priority++; if (flags.show_answer == 1) - printf("right answer: %ls", next_card->answer); + printf("right answer: %ls\n", next_card->answer); } next_card->last_appearance = i; ht_iterate(cards, select_next_card); usleep(500000); + +prompt: printf("\e[1;1H\e[2J"); /* clear the screen */ printf("[%d/%d]", success, i); printf("\t%ls\n", next_card->question); diff --git a/cards.csv b/cards.csv @@ -0,0 +1,73 @@ +ば,ba, +ぱ,pa, +ひ,hi, +び,bi, +ぴ,pi, +ふ,fu, +ぶ,bu, +ぷ,pu, +へ,he, +べ,be, +ぺ,pe, +ほ,ho, +ぼ,bo, +ぽ,po, +ま,ma, +み,mi, +む,mu, +め,me, +も,mo, +や,ya, +ゆ,yu, +よ,yo, +ら,ra, +り,ri, +る,ru, +れ,re, +ろ,ro, +わ,wa, +を,wo, +ん,n, +ゔ,vu, +じゃ,jya, +じゅ,jyu, +じょ,jyo, +あ,a, +い,i, +う,u, +え,e, +お,o, +か,ka, +が,ga, +き,ki, +ぎ,gi, +く,ku, +ぐ,gu, +け,ke, +げ,ge, +こ,ko, +ご,go, +さ,sa, +ざ,za, +し,shi, +じ,ji, +す,su, +ず,zu, +せ,se, +ぜ,ze, +そ,so, +ぞ,zo, +た,ta, +だ,da, +つ,tsu, +づ,zu, +て,te, +で,de, +と,to, +ど,do, +な,na, +に,ni, +ぬ,nu, +ね,ne, +の,no, +は,ha, diff --git a/csv.c b/csv.c @@ -0,0 +1,158 @@ +#include <stdlib.h> +#include <string.h> + +void free_csv_line( char **parsed ) +{ + char **ptr; + + for ( ptr = parsed; *ptr; ptr++ ) { + free( *ptr ); + } + + free( parsed ); +} + +static int count_fields( const char *line ) +{ + const char *ptr; + int cnt, fQuote; + + for ( cnt = 1, fQuote = 0, ptr = line; *ptr; ptr++ ) + { + if ( fQuote ) + { + if ( *ptr == '\"' ) + { + if ( ptr[1] == '\"' ) + { + ptr++; + continue; + } + fQuote = 0; + } + continue; + } + + switch( *ptr ) + { + case '\"': + fQuote = 1; + continue; + case ',': + cnt++; + continue; + default: + continue; + } + } + + if ( fQuote ) { + return -1; + } + + return cnt; +} + +/* + * Given a string containing no linebreaks, or containing line breaks + * which are escaped by "double quotes", extract a NULL-terminated + * array of strings, one for every cell in the row. + */ +char **parse_csv( const char *line ) +{ + char **buf, **bptr, *tmp, *tptr; + const char *ptr; + int fieldcnt, fQuote, fEnd; + + fieldcnt = count_fields( line ); + + if ( fieldcnt == -1 ) { + return NULL; + } + + buf = malloc( sizeof(char*) * (fieldcnt+1) ); + + if ( !buf ) { + return NULL; + } + + tmp = malloc( strlen(line) + 1 ); + + if ( !tmp ) + { + free( buf ); + return NULL; + } + + bptr = buf; + + for ( ptr = line, fQuote = 0, *tmp = '\0', tptr = tmp, fEnd = 0; ; ptr++ ) + { + if ( fQuote ) + { + if ( !*ptr ) { + break; + } + + if ( *ptr == '\"' ) + { + if ( ptr[1] == '\"' ) + { + *tptr++ = '\"'; + ptr++; + continue; + } + fQuote = 0; + } + else { + *tptr++ = *ptr; + } + + continue; + } + + switch( *ptr ) + { + case '\"': + fQuote = 1; + continue; + case '\0': + fEnd = 1; + case ',': + *tptr = '\0'; + *bptr = strdup( tmp ); + + if ( !*bptr ) + { + for ( bptr--; bptr >= buf; bptr-- ) { + free( *bptr ); + } + free( buf ); + free( tmp ); + + return NULL; + } + + bptr++; + tptr = tmp; + + if ( fEnd ) { + break; + } else { + continue; + } + + default: + *tptr++ = *ptr; + continue; + } + + if ( fEnd ) { + break; + } + } + + *bptr = NULL; + free( tmp ); + return buf; +}+ \ No newline at end of file diff --git a/csv.h b/csv.h @@ -0,0 +1,12 @@ +#ifndef CSV_DOT_H_INCLUDE_GUARD +#define CSV_DOT_H_INCLUDE_GUARD + +#define CSV_ERR_LONGLINE 0 +#define CSV_ERR_NO_MEMORY 1 + +char **parse_csv( const char *line ); +void free_csv_line( char **parsed ); +char **split_on_unescaped_newlines(const char *txt); +char *fread_csv_line(FILE *fp, int max_line_size, int *done, int *err); + +#endif diff --git a/fread_csv_line.c b/fread_csv_line.c @@ -0,0 +1,107 @@ +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include "csv.h" + +#define READ_BLOCK_SIZE 65536 +#define QUICK_GETC( ch, fp )\ +do\ +{\ + if ( read_ptr == read_end )\ + {\ + fread_len = fread( read_buf, sizeof(char), READ_BLOCK_SIZE, fp );\ + if ( fread_len < READ_BLOCK_SIZE )\ + read_buf[fread_len] = '\0';\ + read_ptr = read_buf;\ + }\ + ch = *read_ptr++;\ +}\ +while(0) + +/* + * Given a file pointer, read a CSV line from that file. + * File may include newlines escaped with "double quotes". + * + * Warning: This function is optimized for the use case where + * you repeatedly call it until the file is exhausted. It is + * very suboptimal for the use case of just grabbing one single + * line of CSV and stopping. Also, this function advances the + * file position (in the fseek/ftell sense) unpredictably. You + * should not change the file position between calls to + * fread_csv_line (e.g., don't use "getc" on the file in between + * calls to fread_csv_line). + * + * Other arguments: + * size_t max_line_size: Maximum line size, in bytes. + * int *done: Pointer to an int that will be set to 1 when file is exhausted. + * int *err: Pointer to an int where error code will be written. + * + * Warning: Calling this function on an exhausted file (as indicated by the + * 'done' flag) is undefined behavior. + * + * See csv.h for definitions of error codes. + */ +char *fread_csv_line(FILE *fp, int max_line_size, int *done, int *err) { + static FILE *bookmark; + static char read_buf[READ_BLOCK_SIZE], *read_ptr, *read_end; + static int fread_len, prev_max_line_size = -1; + static char *buf; + char *bptr, *limit; + char ch; + int fQuote; + + if ( max_line_size > prev_max_line_size ) { + if ( prev_max_line_size != -1 ) { + free( buf ); + } + buf = malloc( max_line_size + 1 ); + if ( !buf ) { + *err = CSV_ERR_NO_MEMORY; + prev_max_line_size = -1; + return NULL; + } + prev_max_line_size = max_line_size; + } + bptr = buf; + limit = buf + max_line_size; + + if ( bookmark != fp ) { + read_ptr = read_end = read_buf + READ_BLOCK_SIZE; + bookmark = fp; + } + + for ( fQuote = 0; ; ) { + QUICK_GETC(ch, fp); + + if ( !ch || (ch == '\n' && !fQuote)) { + break; + } + + if ( bptr >= limit ) { + free( buf ); + *err = CSV_ERR_LONGLINE; + return NULL; + } + *bptr++ = ch; + + if ( fQuote ) { + if ( ch == '\"' ) { + QUICK_GETC(ch, fp); + + if ( ch != '\"' ) { + if ( !ch || ch == '\n' ) { + break; + } + fQuote = 0; + } + *bptr++ = ch; + } + } else if ( ch == '\"' ) { + fQuote = 1; + } + } + + *done = !ch; + *bptr = '\0'; + return strdup(buf); +}+ \ No newline at end of file diff --git a/readme b/readme @@ -1,3 +1,8 @@ See https://soykaf.me/2019-03-28-exg.html +Edit cards.csv and save your session by typing !save while studying + +This project includes files from csv_parser + (https://github.com/semitrivial/csv_parser) + Known to compile and run on Debian Stretch and FreeBSD 12 diff --git a/split.c b/split.c @@ -0,0 +1,85 @@ +#include <stdlib.h> +#include <string.h> + +/* + * Given a string which might contain unescaped newlines, split it up into + * lines which do not contain unescaped newlines, returned as a + * NULL-terminated array of malloc'd strings. + */ +char **split_on_unescaped_newlines(const char *txt) { + const char *ptr, *lineStart; + char **buf, **bptr; + int fQuote, nLines; + + /* First pass: count how many lines we will need */ + for ( nLines = 1, ptr = txt, fQuote = 0; *ptr; ptr++ ) { + if ( fQuote ) { + if ( *ptr == '\"' ) { + if ( ptr[1] == '\"' ) { + ptr++; + continue; + } + fQuote = 0; + } + } else if ( *ptr == '\"' ) { + fQuote = 1; + } else if ( *ptr == '\n' ) { + nLines++; + } + } + + buf = malloc( sizeof(char*) * (nLines+1) ); + + if ( !buf ) { + return NULL; + } + + /* Second pass: populate results */ + lineStart = txt; + for ( bptr = buf, ptr = txt, fQuote = 0; ; ptr++ ) { + if ( fQuote ) { + if ( *ptr == '\"' ) { + if ( ptr[1] == '\"' ) { + ptr++; + continue; + } + fQuote = 0; + continue; + } else if ( *ptr ) { + continue; + } + } + + if ( *ptr == '\"' ) { + fQuote = 1; + } else if ( *ptr == '\n' || !*ptr ) { + size_t len = ptr - lineStart; + + if ( len == 0 ) { + *bptr = NULL; + return buf; + } + + *bptr = malloc( len + 1 ); + + if ( !*bptr ) { + for ( bptr--; bptr >= buf; bptr-- ) { + free( *bptr ); + } + free( buf ); + return NULL; + } + + memcpy( *bptr, lineStart, len ); + (*bptr)[len] = '\0'; + + if ( *ptr ) { + lineStart = ptr + 1; + bptr++; + } else { + bptr[1] = NULL; + return buf; + } + } + } +}+ \ No newline at end of file