Declarative CSV codecs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add update_col and delete_col to csvt

Typed column updates: decode → transform → re-encode a specific
column by name. delete_col removes a column from header and row.
4 new tests (50 total).

+107
+46
lib/csvt.ml
··· 242 242 243 243 let get_col name t = Row.(obj Fun.id |> col name t ~enc:Fun.id |> finish) 244 244 245 + (* {1 Update support} *) 246 + 247 + let find_col_by_name header name = 248 + let name = String.trim name in 249 + let n = Array.length header in 250 + let rec go i = 251 + if i >= n then -1 252 + else if String.equal (String.trim header.(i)) name then i 253 + else go (i + 1) 254 + in 255 + go 0 256 + 257 + let update_col name codec f header row = 258 + let idx = find_col_by_name header name in 259 + if idx < 0 then Error (Missing_column name) 260 + else if idx >= Array.length row then 261 + Error 262 + (Truncated_row { row = 0; expected = idx + 1; got = Array.length row }) 263 + else 264 + let s = row.(idx) in 265 + match decode_field codec s with 266 + | Error msg -> Error (Bad_value { row = 0; column = name; value = s; msg }) 267 + | Ok v -> 268 + let v' = f v in 269 + let result = Array.copy row in 270 + result.(idx) <- encode_field codec v'; 271 + Ok result 272 + 273 + let delete_col name header row = 274 + let idx = find_col_by_name header name in 275 + if idx < 0 then (header, row) 276 + else 277 + let n = Array.length header in 278 + let new_header = 279 + Array.init (n - 1) (fun i -> 280 + if i < idx then header.(i) else header.(i + 1)) 281 + in 282 + let nr = Array.length row in 283 + let new_row = 284 + Array.init 285 + (max 0 (nr - 1)) 286 + (fun i -> 287 + if i < idx then row.(i) else if i + 1 < nr then row.(i + 1) else "") 288 + in 289 + (new_header, new_row) 290 + 245 291 (* {1 Header resolution} *) 246 292 247 293 type header = string array
+20
lib/csvt.mli
··· 157 157 val decode_row : 'a resolved -> int -> string array -> ('a, error) result 158 158 (** [decode_row resolved row_num fields] decodes a single CSV row. *) 159 159 160 + (** {1:update Update} *) 161 + 162 + val update_col : 163 + string -> 164 + 'a t -> 165 + ('a -> 'a) -> 166 + header -> 167 + string array -> 168 + (string array, error) result 169 + (** [update_col name codec f header row] decodes column [name] from [row] using 170 + [codec], applies [f] to the typed value, re-encodes, and returns the updated 171 + row. Other columns are unchanged. *) 172 + 173 + val delete_col : string -> header -> string array -> header * string array 174 + (** [delete_col name header row] removes the column [name] from both the header 175 + and the row. Returns the new header and row. If [name] is not found, returns 176 + both unchanged. *) 177 + 178 + (** {1:encode Encoding} *) 179 + 160 180 val encode_header : 'a t -> header 161 181 (** [encode_header t] returns the CSV header for encoding. *) 162 182
+41
test/test_csvt.ml
··· 672 672 Alcotest.(check (list string)) "col_names" [ "score" ] (Csvt.col_names codec); 673 673 Alcotest.(check int) "col_count" 1 (Csvt.col_count codec) 674 674 675 + let test_update_col_int () = 676 + let header = [| "id"; "name"; "score" |] in 677 + let row = [| "1"; "alice"; "95" |] in 678 + match Csvt.update_col "score" Csvt.int (fun x -> x + 5) header row with 679 + | Ok row' -> 680 + Alcotest.(check string) "id unchanged" "1" row'.(0); 681 + Alcotest.(check string) "name unchanged" "alice" row'.(1); 682 + Alcotest.(check string) "score updated" "100" row'.(2) 683 + | Error e -> Alcotest.failf "update_col: %s" (Csvt.error_to_string e) 684 + 685 + let test_update_col_missing () = 686 + let header = [| "id"; "name" |] in 687 + let row = [| "1"; "alice" |] in 688 + match Csvt.update_col "score" Csvt.int (fun x -> x + 5) header row with 689 + | Ok _ -> Alcotest.fail "should fail on missing column" 690 + | Error (Csvt.Missing_column "score") -> () 691 + | Error e -> Alcotest.failf "wrong error: %s" (Csvt.error_to_string e) 692 + 693 + let test_delete_col () = 694 + let header = [| "id"; "name"; "score" |] in 695 + let row = [| "1"; "alice"; "95" |] in 696 + let header', row' = Csvt.delete_col "name" header row in 697 + Alcotest.(check int) "header len" 2 (Array.length header'); 698 + Alcotest.(check int) "row len" 2 (Array.length row'); 699 + Alcotest.(check string) "h0" "id" header'.(0); 700 + Alcotest.(check string) "h1" "score" header'.(1); 701 + Alcotest.(check string) "r0" "1" row'.(0); 702 + Alcotest.(check string) "r1" "95" row'.(1) 703 + 704 + let test_delete_col_missing () = 705 + let header = [| "id"; "name" |] in 706 + let row = [| "1"; "alice" |] in 707 + let header', row' = Csvt.delete_col "score" header row in 708 + Alcotest.(check int) "header unchanged" 2 (Array.length header'); 709 + Alcotest.(check int) "row unchanged" 2 (Array.length row') 710 + 675 711 let suite = 676 712 ( "csvt", 677 713 [ ··· 735 771 Alcotest.test_case "get_col missing" `Quick test_get_col_missing; 736 772 Alcotest.test_case "get_col encode" `Quick test_get_col_encode; 737 773 Alcotest.test_case "get_col col_names" `Quick test_get_col_col_names; 774 + (* Update *) 775 + Alcotest.test_case "update_col int" `Quick test_update_col_int; 776 + Alcotest.test_case "update_col missing" `Quick test_update_col_missing; 777 + Alcotest.test_case "delete_col" `Quick test_delete_col; 778 + Alcotest.test_case "delete_col missing" `Quick test_delete_col_missing; 738 779 ] )