Current section

Files

Jump to
tdata src tdata_sheet.erl
Raw

src/tdata_sheet.erl

-module(tdata_sheet).
%% API
-export([
load_sheets/3
]).
-export_type([load_sheets_opts/0]).
-type field_name() :: atom().
-type field_check_opt() ::
check_integer |
check_float |
check_not_empty |
cell_trans_punctuation |
function().
-type check_field() :: {field_name(), [field_check_opt()]}.
-type sheet_name() :: binary() | all.
-type load_sheet_opts() :: #{
skip_comments := boolean() | pos_integer(), %% default false
groups := [field_name()],
checks := [check_field()]
}.
-type load_sheets_opts() :: #{sheet_name() => load_sheet_opts()}.
%%====================================================================
%% API functions
%%====================================================================
-spec load_sheets(PythonPid :: pid(), ExcelFile :: file:filename(), load_sheets_opts()) ->
{ok, Data :: map()} | tuple().
load_sheets(PythonPid, ExcelFile, LoadSheetsOpts) when is_map(LoadSheetsOpts) ->
Data = python:call(PythonPid, load_data, load_excel, [unicode:characters_to_binary(ExcelFile)]),
case [Sheet || Sheet <- Data, filter_empty_rows_sheet(Sheet)] of
[] ->
{empty_sheets, ExcelFile};
Sheets ->
loop_all_sheet_name(LoadSheetsOpts, maps:from_list(Sheets))
end;
load_sheets(_PythonPid, ExcelFile, LoadSheetsOpts) ->
{load_sheets_opts_not_map, ExcelFile, LoadSheetsOpts}.
%%====================================================================
%% Internal functions
%%====================================================================
filter_empty_rows_sheet({undefined, _Rows}) -> false;
filter_empty_rows_sheet({[], _Rows}) -> false;
filter_empty_rows_sheet({_SheetName, undefined}) -> false;
filter_empty_rows_sheet({_SheetName, []}) -> false;
filter_empty_rows_sheet({_SheetName, _Rows}) -> true.
loop_all_sheet_name(#{all := LoadSheetOpts} = LoadSheetsOpts, Sheets) ->
AllLoadSheetsOpts = maps:from_list([{SheetName, LoadSheetOpts} || SheetName <- maps:keys(Sheets)]),
NewLoadSheetsOpts = maps:merge(AllLoadSheetsOpts, maps:remove(all, LoadSheetsOpts)),
loop_all_sheet_name(NewLoadSheetsOpts, Sheets);
loop_all_sheet_name(LoadSheetsOpts, Sheets) when is_map(LoadSheetsOpts) ->
loop_all_sheet_name_do(maps:to_list(LoadSheetsOpts), Sheets, #{}).
loop_all_sheet_name_do([{SheetName, LoadSheetOpts} | LoadSheetsOpts], Sheets, Data)
when is_binary(SheetName) andalso is_map(LoadSheetOpts) ->
case sheet_name(SheetName, Sheets, LoadSheetOpts) of
{ok, SheetData} ->
loop_all_sheet_name_do(LoadSheetsOpts, Sheets, Data#{SheetName => SheetData});
Err -> Err
end;
loop_all_sheet_name_do([{SheetName, _LoadSheetOpts} | _], _Sheets, _Data) when not is_binary(SheetName) ->
{error_sheet_name, SheetName};
loop_all_sheet_name_do([{SheetName, LoadSheetOpts} | _], _Sheets, _Data) when not is_map(LoadSheetOpts) ->
{error_load_sheet_opts, SheetName, LoadSheetOpts};
loop_all_sheet_name_do([], _Sheets, Data) -> {ok, Data}.
sheet_name(SheetName, Sheets, LoadSheetOpts) ->
case maps:find(SheetName, Sheets) of
{ok, Rows} ->
Data = #{sheet_name => SheetName, rows => Rows},
case skip_comments(LoadSheetOpts, Data) of
{ok, _} = Res -> Res;
Err ->
{do_sheet_config_err, SheetName, Err}
end;
error ->
{unfound_sheet, SheetName}
end.
skip_comments(#{skip_comments := true} = LoadSheetOpts, Data) -> %% N=1
#{rows := [Comments0 | Rows]} = Data,
Comments = lists:map(fun get_cell_value/1, Comments0),
NewData = Data#{comments => Comments, rows => Rows},
zip_header(NewData, LoadSheetOpts);
skip_comments(#{skip_comments := N} = LoadSheetOpts, Data) when is_integer(N) ->
#{rows := Rows0} = Data,
{CommentsRows, Rows} = lists:split(N, Rows0),
Comments = [lists:map(fun get_cell_value/1, Comments0) || Comments0 <- CommentsRows],
NewData = Data#{comments => Comments, rows => Rows},
zip_header(NewData, LoadSheetOpts);
skip_comments(LoadSheetOpts, Data) -> %% false
zip_header(Data, LoadSheetOpts).
zip_header(#{rows := [Header0 | Rows0]} = Data, LoadSheetOpts) ->
case check_header(Header0) of
{ok, Header} ->
Rows = [maps:from_list(lists:zip(Header, Row)) || Row <- Rows0],
check_data_and_groups(Data#{header => Header, rows => Rows}, LoadSheetOpts);
Err -> Err
end.
check_data_and_groups(Data, LoadSheetOpts) ->
Checks = maps:get(checks, LoadSheetOpts, []),
case maps:get(groups, LoadSheetOpts, []) of
[] ->
check_data(Data, Checks);
Groups0 ->
#{header := Header} = Data,
Groups = lists:flatten(Groups0),
case Groups -- Header of
[] ->
{GroupChecks, NewChecks} =
lists:splitwith(
fun(Check) ->
lists:member(element(1, Check), Groups)
end, Checks),
case check_data(Data, Groups, NewChecks) of
{ok, NewData} ->
groups(Groups0, NewData, GroupChecks);
Err -> Err
end;
MissGroups ->
{undefined_groups, MissGroups}
end
end.
check_data(Data, Checks) ->
check_data(Data, [], Checks).
check_data(#{header := Header, rows := Rows} = Data, FilterFields, Checks) ->
NewHeader = [Field || Field <- Header, not lists:member(Field, FilterFields)],
case check_cols_map(NewHeader, Checks, Rows) of
{ok, NewRows} ->
{ok, Data#{rows => NewRows}};
Err -> Err
end.
check_cols_map([Field | Header], Checks, Rows) ->
case check_rows_map(Field, Rows, Checks, []) of
{ok, NewRows} ->
check_cols_map(Header, Checks, NewRows);
Err -> Err
end;
check_cols_map([], _Checks, NewRows) ->
{ok, NewRows}.
check_rows_map(Field, [Row | Rows], Checks, Acc) ->
CheckList = get_field_checks(Field, Checks),
Cell0 = maps:get(Field, Row),
case get_cell_value(Cell0, CheckList) of
{ok, CellValue} ->
NewRow = Row#{Field => CellValue},
check_rows_map(Field, Rows, Checks, [NewRow | Acc]);
Err -> Err
end;
check_rows_map(_Field, [], _Checks, Acc) ->
{ok, lists:reverse(Acc)}.
groups(Groups, #{rows := Rows} = Data, Checks) ->
case groups_map(Groups, Rows, Checks) of
{ok, NewRows} ->
{ok, Data#{rows := NewRows}};
Err -> Err
end.
groups_map([[GroupKey | SameGroups] | Groups], Rows, Checks) when is_atom(GroupKey) ->
case groups_map_first_row(GroupKey, SameGroups, Rows, Checks) of
{ok, GroupAcc} ->
GroupList = maps:to_list(GroupAcc),
case groups_map_do(GroupList, Groups, Checks, #{}) of
{ok, Data} ->
{ok, #{GroupKey => Data}};
Err -> Err
end;
Err -> Err
end;
groups_map([GroupKey | Groups], Rows, Checks) when is_atom(GroupKey) ->
groups_map([[GroupKey] | Groups], Rows, Checks);
groups_map([], Rows, _Checks) -> {ok, Rows}.
groups_map_do([{Key, Rows0} | GroupList], Groups, Checks, Acc) ->
Rows = lists:reverse(Rows0),
case groups_map(Groups, Rows, Checks) of
{ok, Data} ->
groups_map_do(GroupList, Groups, Checks, Acc#{Key => Data});
Err -> Err
end;
groups_map_do([], _Groups, _Checks, Acc) -> {ok, Acc}.
groups_map_first_row(GroupKey, SameGroups, [Row | Rows], Checks) ->
Cell = maps:get(GroupKey, Row),
case is_cell_empty(Cell) of
false ->
FieldCheckList = get_field_checks(GroupKey, Checks),
SameGroupChecks = get_fields_checks(SameGroups, Checks),
groups_map_same_groups(GroupKey, Cell, Row, Rows, FieldCheckList, SameGroupChecks, #{});
true ->
{first_row_is_empty, GroupKey}
end.
groups_map_same_groups(GroupKey, Cell0, Row, Rows, FieldCheckList, SameGroupChecks, Acc) ->
case get_cell_value(Cell0, FieldCheckList) of
{ok, CellValue} ->
case get_cells_map(SameGroupChecks, Row, #{GroupKey => CellValue}) of
{ok, DefaultRow} ->
NewRow = maps:merge(Row, DefaultRow),
groups_map_rows(GroupKey, Rows, CellValue, DefaultRow, FieldCheckList, SameGroupChecks,
Acc#{CellValue => [NewRow]});
Err -> Err
end;
Err -> Err
end.
groups_map_rows(GroupKey, [Row | Rows], LastCell, DefaultRow, FieldCheckList, SameGroupChecks, Acc) ->
Cell = maps:get(GroupKey, Row),
case is_cell_empty(Cell) of
false ->
groups_map_same_groups(GroupKey, Cell, Row, Rows, FieldCheckList, SameGroupChecks, Acc);
true ->
NewRow = maps:merge(Row, DefaultRow),
NewAcc = Acc#{LastCell => [NewRow | maps:get(LastCell, Acc)]},
groups_map_rows(GroupKey, Rows, LastCell, DefaultRow, FieldCheckList, SameGroupChecks, NewAcc)
end;
groups_map_rows(_GroupKey, [], _LastCell, _DefaultRow, _FieldCheckList, _SameGroupChecks, Acc) ->
{ok, Acc}.
check_header([]) -> empty_header;
check_header(Header) -> get_header(Header, []).
get_header([{Row, Col, Type, Cell} | Header], NewHeader) ->
case catch binary_to_atom(Cell, utf8) of
{'EXIT', Reason} ->
{header_trans_to_atom, [
{row, Row}, {col, Col}, {type, Type}, {cell, Cell}, {err, Reason}
]};
Field ->
get_header(Header, [Field | NewHeader])
end;
get_header([], Header) -> {ok, lists:reverse(Header)}.
is_empty(<<>>) -> true;
is_empty([]) -> true;
is_empty(_) -> false.
is_cell_empty(Cell) ->
is_empty(get_cell_value(Cell)).
get_cell_value({_Row, _Col, _Type, Cell}) -> Cell.
get_field_checks(Field, Checks) ->
proplists:get_value(Field, Checks, []).
get_fields_checks(Fields, Checks) ->
get_fields_checks(Fields, Checks, []).
get_fields_checks([Field | Fields], Checks, Acc) ->
get_fields_checks(Fields, Checks, [{Field, get_field_checks(Field, Checks)} | Acc]);
get_fields_checks([], _Checks, Acc) -> Acc.
get_cells_map([{Field, FieldCheckList} | CheckFields], Row, Acc) ->
case get_cell_value(maps:get(Field, Row), FieldCheckList) of
{ok, Cell} ->
get_cells_map(CheckFields, Row, Acc#{Field => Cell});
Err -> Err
end;
get_cells_map([], _Row, Acc) -> {ok, Acc}.
get_cell_value({_Row, _Col, _Type, Cell}, []) -> {ok, Cell};
get_cell_value({Row, Col, Type, Cell}, CheckList) ->
NewCheckList = [Check || Check <- CheckList, is_function(Check, 1) orelse
is_function(Check, 2) orelse is_atom(Check)],
case check_fun_cell(NewCheckList, Type, Cell) of
{ok, _} = Res -> Res;
Err ->
{check_field_fail, [
{row, Row}, {col, Col}, {type, Type}, {cell, Cell}, {err, Err}
]}
end.
check_fun_cell([Check | CheckList], Type, Cell) when is_function(Check, 1) ->
case Check(Cell) of
{ok, NewCell} ->
check_fun_cell(CheckList, Type, NewCell);
Err -> Err
end;
check_fun_cell([Check | CheckList], Type, Cell) when is_function(Check, 2) ->
case Check(Type, Cell) of
{ok, NewCell} ->
check_fun_cell(CheckList, Type, NewCell);
Err -> Err
end;
check_fun_cell([Check | CheckList], Type, Cell) when is_atom(Check) ->
code:ensure_loaded(tdata_util),
case erlang:function_exported(tdata_util, Check, 1) of
true ->
case tdata_util:Check(Cell) of
{ok, NewCell} ->
check_fun_cell(CheckList, Type, NewCell);
Err -> Err
end;
false ->
case erlang:function_exported(tdata_util, Check, 2) of
true ->
case tdata_util:Check(Type, Cell) of
{ok, NewCell} ->
check_fun_cell(CheckList, Type, NewCell);
Err -> Err
end;
false ->
{undefined_check_fun, Check}
end
end;
check_fun_cell([], _Type, Cell) -> {ok, Cell}.