From 960db59183d3b121a773f972ffe8dcbf5dcdfbec Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Tue, 10 Dec 2024 14:13:02 +0000 Subject: [PATCH] sheet: Add pivot builtin command The user can either pivot on the column under the cursor with 'v' or enter an SQL group-by expression with V --- app/sheet.c | 5 +- app/sheet/file.h | 1 + app/sheet/key-bindings.c | 2 + app/sheet/pivot.c | 309 ++++++++++++++++++ app/sheet/procedure.h | 2 + app/sheet/ui_buffer.c | 6 + app/test/Makefile | 16 +- .../expected/test-sheet-pivot-1-drilldown.out | 25 ++ .../expected/test-sheet-pivot-1-groups.out | 25 ++ .../expected/test-sheet-pivot-1-indexed.out | 25 ++ app/test/expected/test-sheet-pivot-1.out | 25 ++ 11 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 app/sheet/pivot.c create mode 100644 app/test/expected/test-sheet-pivot-1-drilldown.out create mode 100644 app/test/expected/test-sheet-pivot-1-groups.out create mode 100644 app/test/expected/test-sheet-pivot-1-indexed.out create mode 100644 app/test/expected/test-sheet-pivot-1.out diff --git a/app/sheet.c b/app/sheet.c index 0a0c6972..1f3b58d0 100644 --- a/app/sheet.c +++ b/app/sheet.c @@ -538,6 +538,7 @@ static zsvsheet_status zsvsheet_help_handler(struct zsvsheet_proc_context *ctx) return stat; } +#include "sheet/pivot.c" #include "sheet/newline_handler.c" /* We do most procedures in one handler. More complex procedures can be @@ -624,7 +625,9 @@ struct builtin_proc_desc { { zsvsheet_builtin_proc_filter, "filter", "Hide rows that do not contain the specified text", zsvsheet_filter_handler }, { zsvsheet_builtin_proc_subcommand, "subcommand", "Editor subcommand", zsvsheet_subcommand_handler }, { zsvsheet_builtin_proc_help, "help", "Display a list of actions and key-bindings", zsvsheet_help_handler }, - { zsvsheet_builtin_proc_newline, "","Follow hyperlink (if any)", zsvsheet_newline_handler }, + { zsvsheet_builtin_proc_newline, "","Follow hyperlink (if any) or drill down", zsvsheet_newline_handler }, + { zsvsheet_builtin_proc_pivot_cur_col, "pivotcur","Group rows by the column under the cursor", zsvsheet_pivot_handler }, + { zsvsheet_builtin_proc_pivot_expr, "pivotexpr","Group rows with group-by SQL expression", zsvsheet_pivot_handler }, { -1, NULL, NULL, NULL } }; /* clang-format on */ diff --git a/app/sheet/file.h b/app/sheet/file.h index 482db958..d1df2227 100644 --- a/app/sheet/file.h +++ b/app/sheet/file.h @@ -7,5 +7,6 @@ int zsvsheet_ui_buffer_open_file(const char *filename, const struct zsv_opts *zs struct zsvsheet_ui_buffer **ui_buffer_stack_top); zsvsheet_status zsvsheet_open_file_opts(struct zsvsheet_proc_context *ctx, struct zsvsheet_ui_buffer_opts *opts); +zsvsheet_status zsvsheet_open_file(struct zsvsheet_proc_context *ctx, const char *filepath, struct zsv_opts *zopts); #endif diff --git a/app/sheet/key-bindings.c b/app/sheet/key-bindings.c index c1b5bdc7..6eff5049 100644 --- a/app/sheet/key-bindings.c +++ b/app/sheet/key-bindings.c @@ -174,6 +174,8 @@ struct zsvsheet_key_binding zsvsheet_vim_key_bindings[] = { { .ch = '?', .proc_id = zsvsheet_builtin_proc_help, }, { .ch = '\n', .proc_id = zsvsheet_builtin_proc_newline, }, { .ch = '\r', .proc_id = zsvsheet_builtin_proc_newline, }, + { .ch = 'v', .proc_id = zsvsheet_builtin_proc_pivot_cur_col, }, + { .ch = 'V', .proc_id = zsvsheet_builtin_proc_pivot_expr, }, { .ch = -1 } }; diff --git a/app/sheet/pivot.c b/app/sheet/pivot.c new file mode 100644 index 00000000..f6075259 --- /dev/null +++ b/app/sheet/pivot.c @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2021 Liquidaty and zsv contributors. All rights reserved. + * This file is part of zsv/lib, distributed under the license defined at + * https://opensource.org/licenses/MIT + */ + +#include +#include +#include "../external/sqlite3/sqlite3.h" +#include +#include +#include +#include +#include +#include "file.h" +#include "handlers_internal.h" +#include "../curses.h" +#include "../sql_internal.h" + +struct pivot_row { + char *value; // to do: this will be the drill-down criteria +}; + +struct pivot_data { + char *value_sql; // the sql expression entered by the user e.g. City + char *data_filename; + struct { + struct pivot_row *data; // for each row, the value of the sql expression e.g. New York + size_t capacity; + size_t used; + } rows; +}; + +static void pivot_data_delete(void *h) { + struct pivot_data *pd = h; + if (pd) { + for (size_t i = 0; i < pd->rows.used; i++) + free(pd->rows.data[i].value); + free(pd->rows.data); + free(pd->value_sql); + free(pd->data_filename); + free(pd); + } +} + +static struct pivot_data *pivot_data_new(const char *data_filename, const char *value_sql) { + struct pivot_data *pd = calloc(1, sizeof(*pd)); + if (pd && (pd->value_sql = strdup(value_sql)) && (pd->data_filename = strdup(data_filename))) + return pd; + pivot_data_delete(pd); + return NULL; +} + +#define ZSV_MYSHEET_PIVOT_DATA_ROWS_INITIAL 32 +static int pivot_data_grow(struct pivot_data *pd) { + if (pd->rows.used == pd->rows.capacity) { + size_t new_capacity = pd->rows.capacity == 0 ? ZSV_MYSHEET_PIVOT_DATA_ROWS_INITIAL : pd->rows.capacity * 2; + struct pivot_row *new_data = realloc(pd->rows.data, new_capacity * sizeof(*pd->rows.data)); + if (!new_data) + return ENOMEM; + pd->rows.data = new_data; + pd->rows.capacity = new_capacity; + } + return 0; +} + +static int add_pivot_row(struct pivot_data *pd, const char *value, size_t len) { + int err = pivot_data_grow(pd); + char *value_dup = NULL; + if (!err && value && len) { + value_dup = malloc(len + 1); + if (value_dup) { + memcpy(value_dup, value, len); + value_dup[len] = '\0'; + } + } + pd->rows.data[pd->rows.used++].value = value_dup; + return err; +} + +static struct pivot_row *get_pivot_row_data(struct pivot_data *pd, size_t row_ix) { + if (pd && row_ix < pd->rows.used) + return &pd->rows.data[row_ix]; + return NULL; +} + +// TO DO: return zsvsheet_status +static enum zsv_ext_status get_cell_attrs(void *pdh, int *attrs, size_t start_row, size_t row_count, size_t cols) { + struct pivot_data *pd = pdh; + size_t end_row = start_row + row_count; + int attr = 0; + +#ifdef A_BOLD + attr |= A_BOLD; +#endif +// Absent on Mac OSX 13 +#ifdef A_ITALIC + attr |= A_ITALIC; +#endif + + if (end_row > pd->rows.used) + end_row = pd->rows.used; + for (size_t i = start_row; i < end_row; i++) + attrs[i * cols] = attr; + return zsv_ext_status_ok; +} + +static void pivot_on_header_cell(void *ctx, size_t col_ix, const char *colname) { + (void)colname; + if (col_ix == 0) + add_pivot_row(ctx, NULL, 0); +} + +static void pivot_on_data_cell(void *ctx, size_t col_ix, const char *text, size_t len) { + if (col_ix == 0) + add_pivot_row(ctx, text, len); +} + +static zsvsheet_status zsv_sqlite3_to_csv(zsvsheet_proc_context_t pctx, struct zsv_sqlite3_db *zdb, const char *sql, + void *ctx, void (*on_header_cell)(void *, size_t, const char *), + void (*on_data_cell)(void *, size_t, const char *, size_t len)) { + const char *err_msg = NULL; + zsvsheet_status zst = zsvsheet_status_error; + sqlite3_stmt *stmt = NULL; + + if ((zdb->rc = sqlite3_prepare_v2(zdb->db, sql, -1, &stmt, NULL)) == SQLITE_OK) { + char *tmp_fn = zsv_get_temp_filename("zsv_mysheet_ext_XXXXXXXX"); + struct zsv_csv_writer_options writer_opts = zsv_writer_get_default_opts(); + zsv_csv_writer cw = NULL; + if (!tmp_fn) + zst = zsvsheet_status_memory; + else if (!(writer_opts.stream = fopen(tmp_fn, "wb"))) { + zst = zsvsheet_status_error; + err_msg = strerror(errno); + } else if (!(cw = zsv_writer_new(&writer_opts))) + zst = zsvsheet_status_memory; + else { + zst = zsvsheet_status_ok; + unsigned char cw_buff[1024]; + zsv_writer_set_temp_buff(cw, cw_buff, sizeof(cw_buff)); + + int col_count = sqlite3_column_count(stmt); + // write header row + for (int i = 0; i < col_count; i++) { + const char *colname = sqlite3_column_name(stmt, i); + zsv_writer_cell(cw, !i, (const unsigned char *)colname, colname ? strlen(colname) : 0, 1); + if (on_header_cell) + on_header_cell(ctx, i, colname); + } + + // write sql results + while (sqlite3_step(stmt) == SQLITE_ROW) { + for (int i = 0; i < col_count; i++) { + const unsigned char *text = sqlite3_column_text(stmt, i); + int len = text ? sqlite3_column_bytes(stmt, i) : 0; + zsv_writer_cell(cw, !i, text, len, 1); + if (on_data_cell) + on_data_cell(ctx, i, (const char *)text, len); + } + } + } + if (cw) + zsv_writer_delete(cw); + if (writer_opts.stream) + fclose(writer_opts.stream); + + if (tmp_fn && zsv_file_exists(tmp_fn)) { + struct zsvsheet_ui_buffer_opts uibopts = {0}; + uibopts.data_filename = tmp_fn; + zst = zsvsheet_open_file_opts(pctx, &uibopts); + } else { + if (zst == zsvsheet_status_ok) { + zst = zsvsheet_status_error; // to do: make this more specific + if (!err_msg && zdb && zdb->rc != SQLITE_OK) + err_msg = sqlite3_errmsg(zdb->db); + } + } + if (zst != zsvsheet_status_ok) + free(tmp_fn); + } + if (stmt) + sqlite3_finalize(stmt); + if (err_msg) + zsvsheet_set_status(ctx, "Error: %s", err_msg); + return zst; +} + +zsvsheet_status pivot_drill_down(zsvsheet_proc_context_t ctx) { + enum zsvsheet_status zst = zsvsheet_status_ok; + zsvsheet_buffer_t buff = zsvsheet_buffer_current(ctx); + struct pivot_data *pd; + struct zsvsheet_rowcol rc; + if (zsvsheet_buffer_get_ctx(buff, (void **)&pd) != zsv_ext_status_ok || + zsvsheet_buffer_get_selected_cell(buff, &rc) != zsvsheet_status_ok) { + return zsvsheet_status_error; + } + struct pivot_row *pr = get_pivot_row_data(pd, rc.row); + if (pd && pd->data_filename && pd->value_sql && pr) { + struct zsv_sqlite3_dbopts dbopts = {0}; + sqlite3_str *sql_str = NULL; + struct zsv_sqlite3_db *zdb = zsv_sqlite3_db_new(&dbopts); + + if (!zdb || !(sql_str = sqlite3_str_new(zdb->db))) + zst = zsvsheet_status_memory; + else if (zdb->rc == SQLITE_OK && zsv_sqlite3_add_csv(zdb, pd->data_filename, NULL, NULL) == SQLITE_OK) { + if (zsvsheet_buffer_info(buff).has_row_num) + sqlite3_str_appendf(sql_str, "select *"); + else + sqlite3_str_appendf(sql_str, "select rowid as [Row #], *"); + sqlite3_str_appendf(sql_str, " from data where \"%w\" = %Q", pd->value_sql, pr->value); + zst = zsv_sqlite3_to_csv(ctx, zdb, sqlite3_str_value(sql_str), NULL, NULL, NULL); + } + + if (sql_str) + sqlite3_free(sqlite3_str_finish(sql_str)); + if (zdb) { + if (zst != zsvsheet_status_ok) { + // to do: consolidate this with same code in sql.c + if (zdb->err_msg) + fprintf(stderr, "Error: %s\n", zdb->err_msg); + else if (!zdb->db) + fprintf(stderr, "Error (unable to open db, code %i): %s\n", zdb->rc, sqlite3_errstr(zdb->rc)); + else if (zdb->rc != SQLITE_OK) + fprintf(stderr, "Error (code %i): %s\n", zdb->rc, sqlite3_errstr(zdb->rc)); + } + zsv_sqlite3_db_delete(zdb); + } + } + return zst; +} + +/** + * Here we define a custom command for the zsv `sheet` feature + */ +static zsvsheet_status zsvsheet_pivot_handler(struct zsvsheet_proc_context *ctx) { + char result_buffer[256] = {0}; + const char *expr; + size_t expr_len; + struct zsvsheet_rowcol rc; + int ch = zsvsheet_ext_keypress(ctx); + if (ch < 0) + return zsvsheet_status_error; + + zsvsheet_buffer_t buff = zsvsheet_buffer_current(ctx); + const char *data_filename = NULL; + if (buff) + data_filename = zsvsheet_buffer_data_filename(buff); + + if (!data_filename) { // TO DO: check that the underlying data is a tabular file and we know how to parse + zsvsheet_set_status(ctx, "Pivot table only available for tabular data buffers"); + return zsvsheet_status_ok; + } + + switch (ctx->proc_id) { + case zsvsheet_builtin_proc_pivot_expr: + zsvsheet_ext_prompt(ctx, result_buffer, sizeof(result_buffer), "Pivot table: Enter group-by SQL expr"); + if (*result_buffer == '\0') + return zsvsheet_status_ok; + expr = result_buffer; + expr_len = strlen(expr); + break; + case zsvsheet_builtin_proc_pivot_cur_col: + if (zsvsheet_buffer_get_selected_cell(buff, &rc) != zsvsheet_status_ok) + return zsvsheet_status_error; + + expr = zsvsheet_ui_buffer_get_header(buff, rc.col); + expr_len = strlen(expr); + expr = (char *)zsv_strtrim((unsigned char *)expr, &expr_len); + assert(expr); + break; + default: + assert(0); + return zsvsheet_status_error; + } + + enum zsvsheet_status zst = zsvsheet_status_ok; + struct zsv_sqlite3_dbopts dbopts = {0}; + struct zsv_opts zopts = zsvsheet_buffer_get_zsv_opts(buff); + struct zsv_sqlite3_db *zdb = zsv_sqlite3_db_new(&dbopts); + sqlite3_str *sql_str = NULL; + struct pivot_data *pd = NULL; + if (!zdb || !(sql_str = sqlite3_str_new(zdb->db))) + zst = zsvsheet_status_memory; + else if (zdb->rc == SQLITE_OK && zsv_sqlite3_add_csv(zdb, data_filename, &zopts, NULL) == SQLITE_OK) { + sqlite3_str_appendf(sql_str, "select \"%.*w\" as value, count(1) as Count from data group by \"%.*w\"", expr_len, + expr, expr_len, expr); + if (!(pd = pivot_data_new(data_filename, expr))) + zst = zsvsheet_status_memory; + else { + zst = zsv_sqlite3_to_csv(ctx, zdb, sqlite3_str_value(sql_str), pd, pivot_on_header_cell, pivot_on_data_cell); + if (zst == zsvsheet_status_ok) { + buff = zsvsheet_buffer_current(ctx); + zsvsheet_buffer_set_ctx(buff, pd, pivot_data_delete); + zsvsheet_buffer_set_cell_attrs(buff, get_cell_attrs); + zsvsheet_buffer_on_newline(buff, pivot_drill_down); + pd = NULL; // so that it isn't cleaned up below + } + } + // TO DO: add param to ext_sheet_open_file to set filename vs data_filename, and set buffer type or proc owner + // TO DO: add way to attach custom context, and custom context destructor, to the new buffer + // TO DO: add cell highlighting + } + + zsv_sqlite3_db_delete(zdb); + if (sql_str) + sqlite3_free(sqlite3_str_finish(sql_str)); + pivot_data_delete(pd); + return zst; +} diff --git a/app/sheet/procedure.h b/app/sheet/procedure.h index 5c36a309..d81ac0c8 100644 --- a/app/sheet/procedure.h +++ b/app/sheet/procedure.h @@ -34,6 +34,8 @@ enum { zsvsheet_builtin_proc_help, zsvsheet_builtin_proc_vim_g_key_binding_dmux, zsvsheet_builtin_proc_newline, + zsvsheet_builtin_proc_pivot_expr, + zsvsheet_builtin_proc_pivot_cur_col, }; #define ZSVSHEET_PROC_INVALID 0 diff --git a/app/sheet/ui_buffer.c b/app/sheet/ui_buffer.c index ebf30d1f..04984145 100644 --- a/app/sheet/ui_buffer.c +++ b/app/sheet/ui_buffer.c @@ -193,3 +193,9 @@ int zsvsheet_ui_buffer_pop(struct zsvsheet_ui_buffer **base, struct zsvsheet_ui_ } return 0; } + +static const char *zsvsheet_ui_buffer_get_header(struct zsvsheet_ui_buffer *uib, size_t col) { + struct zsvsheet_screen_buffer *sb = uib->buffer; + + return (char *)zsvsheet_screen_buffer_cell_display(sb, 0, col); +} diff --git a/app/test/Makefile b/app/test/Makefile index 195503a2..eea9bcfb 100644 --- a/app/test/Makefile +++ b/app/test/Makefile @@ -613,7 +613,7 @@ test-sheet-cleanup: @rm -f tmux-*.log @tmux kill-server || printf '' -test-sheet-all: test-sheet-1 test-sheet-2 test-sheet-3 test-sheet-4 test-sheet-5 test-sheet-6 test-sheet-7 test-sheet-8 test-sheet-9 test-sheet-10 test-sheet-11 test-sheet-12 test-sheet-13 test-sheet-14 test-sheet-subcommand test-sheet-prop-cmd-opt +test-sheet-all: test-sheet-1 test-sheet-2 test-sheet-3 test-sheet-4 test-sheet-5 test-sheet-6 test-sheet-7 test-sheet-8 test-sheet-9 test-sheet-10 test-sheet-11 test-sheet-12 test-sheet-13 test-sheet-14 test-sheet-subcommand test-sheet-prop-cmd-opt test-sheet-pivot @(for SESSION in $^; do ! tmux kill-session -t "$$SESSION" 2>/dev/null; done && ${TEST_PASS} || ${TEST_FAIL}) TMUX_TERM=xterm-256color @@ -802,6 +802,20 @@ test-sheet-prop-cmd-opt: ${BUILD_DIR}/bin/zsv_sheet${EXE} ${BUILD_DIR}/bin/zsv_p # @tmux send-keys -t $@ "q" # @${CMP} ${TMP_DIR}/$@.out expected/$@.out && ${TEST_PASS} || (echo 'Incorrect output:' && cat ${TMP_DIR}/$@.out && ${TEST_FAIL}) +test-sheet-pivot: test-sheet-pivot-1 + +test-sheet-pivot-1: ${BUILD_DIR}/bin/zsv_sheet${EXE} + @${TEST_INIT} + @echo 'set-option default-terminal "${TMUX_TERM}"' > ~/.tmux.conf + @(tmux new-session -x 80 -y 25 -d -s "$@" "${PREFIX} $< worldcitiespop_mil.csv" && \ + ${EXPECT} $@ indexed && \ + tmux send-keys -t $@ l v && \ + ${EXPECT} $@ groups && \ + tmux send-keys -t $@ j j Enter && \ + ${EXPECT} $@ drilldown && \ + tmux send-keys -t $@ G && \ + ${EXPECT} $@ && ${TEST_PASS} || ${TEST_FAIL}) + benchmark-sheet-index: ${BUILD_DIR}/bin/zsv_sheet${EXE} ${TIMINGS_CSV} @${TEST_INIT} @if [ "${BIG_FILE}" = "none" ]; then \ diff --git a/app/test/expected/test-sheet-pivot-1-drilldown.out b/app/test/expected/test-sheet-pivot-1-drilldown.out new file mode 100644 index 00000000..bd14fadf --- /dev/null +++ b/app/test/expected/test-sheet-pivot-1-drilldown.out @@ -0,0 +1,25 @@ +Row # Country City AccentCit Region Populatio Latitude Longitude +546 af abakeh Abakeh 24 36.67363 69.078194 +549 af abakheyl' Abakheyl' 35 34.744167 70.287778 +551 af aband Aband 42 35.474953 69.871144 +556 af abasali Abasali 39 32.813402 65.968042 +558 af abaskhank Abaskhank 29 32.503976 69.209795 +560 af abas khel Abas Khel 28 31.975853 67.59182 +561 af abaskheyl Abaskheyl 28 31.975853 67.59182 +563 af `abaskust `Abaskust 27 34.331309 67.640744 +576 af ab barik Ab Barik 09 34.651024 66.254311 +577 af `abbas `Abbas 08 33.333 67.643278 +582 af `abbaskha `Abbaskha 29 32.469231 69.20712 +583 af `abbas kh `Abbas Kh 29 32.469231 69.20712 +588 af `abbas kh `Abbas Kh 29 32.671879 69.134674 +589 af `abbaskhe `Abbaskhe 29 32.671879 69.134674 +590 af abbaskhel Abbaskhel 29 32.831993 69.12336 +593 af `abbaskhe `Abbaskhe 29 32.831389 69.260556 +594 af abbaskhey Abbaskhey 29 32.671879 69.134674 +596 af `abbaskhe `Abbaskhe 29 32.831993 69.12336 +600 af `abbas ku `Abbas Ku 27 34.331309 67.640744 +604 af abbazha-y Abbazha-y 10 31.791857 64.566416 +607 af ab bazha- Ab Bazha- 10 31.719049 64.600709 +608 af abbazha-y Abbazha-y 10 31.719049 64.600709 +611 af abbedak Abbedak 09 33.482286 64.310608 +? for help 546 diff --git a/app/test/expected/test-sheet-pivot-1-groups.out b/app/test/expected/test-sheet-pivot-1-groups.out new file mode 100644 index 00000000..f173628f --- /dev/null +++ b/app/test/expected/test-sheet-pivot-1-groups.out @@ -0,0 +1,25 @@ +Row # value Count +1 ad 27 +2 ae 147 +3 af 27822 +4 ag 55 +5 ai 14 +6 al 4730 +7 am 896 +8 an 75 +9 ao 6170 +10 ar 2817 +11 at 4740 +12 au 3403 +13 aw 37 +14 az 3522 +15 ba 5076 +16 bb 165 +17 bd 8280 +18 be 5056 +19 bf 3248 +20 bg 6258 +21 bh 102 +22 bi 646 +23 bj 1331 +? for help 1 diff --git a/app/test/expected/test-sheet-pivot-1-indexed.out b/app/test/expected/test-sheet-pivot-1-indexed.out new file mode 100644 index 00000000..610fb180 --- /dev/null +++ b/app/test/expected/test-sheet-pivot-1-indexed.out @@ -0,0 +1,25 @@ +Row # Country City AccentCit Region Populatio Latitude Longitude +1 ir sarmaj-e Sarmaj-e 13 34.3578 47.5207 +2 ad aixirival Aixirival 06 42.466666 1.5 +3 mm mokho-atw Mokho-atw 09 18.033333 96.75 +4 id selingon Selingon 17 -8.8374 116.4914 +5 ir berimvand Berimvand 13 34.2953 47.1096 +6 pl chomiaza Chomiaza 73 52.7508 17.841793 +7 mg itona Itona 02 -23.86666 47.216666 +8 ad andorra-v Andorra-V 07 42.5 1.5166667 +9 mx la tortug La Tortug 25 25.75 -108.3333 +10 us asaph Asaph PA 41.770833 -77.40527 +11 ad andorre-v Andorre-V 07 42.5 1.5166667 +12 ru dolmatova Dolmatova 71 57.436791 63.279522 +13 ro escu Escu 13 47.133333 23.533333 +14 us la presa La Presa CA 35119 32.708055 -116.9963 +15 pk makam khu Makam Khu 04 33.650863 72.551536 +16 ad aubinya Aubinyà 06 42.45 1.5 +17 mm kiosong Kiosöng 11 22.583333 97.05 +18 tr donencay Dönençay 28 40.266667 38.583333 +19 it roncaglia Roncaglia 05 45.05 9.8 +20 ml kourmouss Kourmouss 04 14.75 -5.033333 +21 id lamogo Lamogo 38 -4.3945 119.9028 +22 ad casas vil Casas Vil 03 42.533333 1.5666667 +23 ru otdeleniy Otdeleniy 86 51.726473 39.714345 +? for help 1 diff --git a/app/test/expected/test-sheet-pivot-1.out b/app/test/expected/test-sheet-pivot-1.out new file mode 100644 index 00000000..d2f20474 --- /dev/null +++ b/app/test/expected/test-sheet-pivot-1.out @@ -0,0 +1,25 @@ +Row # Country City AccentCit Region Populatio Latitude Longitude +89222 af zungur ka Zungur Ka 27 33.993056 68.658056 +89223 af zunun Zunun 01 38.438492 70.904387 +89224 af zu ol faq Zu ol Faq 03 35.616782 68.670307 +89225 af zu ol faq Zu ol Faq 27 34.1575 68.501389 +89232 af zur baraw Zur Baraw 34 35.028791 71.507898 +89235 af zuri Zuri 11 33.94062 62.160912 +89238 af zur kalay Zur Kalay 35 34.670345 70.165192 +89244 af zur kowt Zur Kowt 37 33.542222 69.734167 +89246 af zurmatiya Zurmatiya 28 32.138117 66.835273 +89248 af zurmat Zurmat 36 33.437782 69.02774 +89249 af zurni Zurni 09 33.568987 63.289317 +89252 af zurwam Zurwam 23 31.439167 66.936667 +89256 af zutni khe Zutni Khe 36 33.360477 68.973862 +89258 af zutni khe Zutni Khe 36 33.360477 68.973862 +89261 af zu Zu 01 36.84154 71.069323 +89262 af zvaka Zvaka 29 32.968334 68.809853 +89264 af zwaka Zwaka 29 32.968334 68.809853 +89267 af zyan say Zyan Say 07 35.864952 64.278152 +89279 af zyaratjay Zyaratjay 11 34.197386 62.140143 +89281 af zyarat ka Zyarat Ka 18 34.288611 71.138889 +89282 af zyarat ka Zyarat Ka 23 31.355826 66.535299 +89283 af zyarat ka Zyarat Ka 35 34.738611 70.269444 +89286 af zyarat Zyarat 39 32.523283 65.905489 +? for help 89286