1/*  TakeoffMetric US construction estimating constants, dataset version 1.2.0.
    2
    3    A port of the Go module github.com/tresor4k/takeoffmetric-go: the same
    4    table (data/constants.csv), compiled into the module at load time as
    5    read-only facts. Lookup only: the pack computes nothing.
    6    Method: https://takeoffmetric.com/methodology/
    7
    8    Estimating values only: confirm with your plans, your supplier and your
    9    local code before ordering or building.
   10
   11    License: code MIT; constants (data/) see data/LICENSE.
   12*/
   13
   14:- module(takeoffmetric,
   15          [ by_id/2,          % +Id, -Constant
   16            search/2,         % +Query, -Constants
   17            all/1,            % -Constants
   18            sources/1         % -Sources
   19          ]).   20:- use_module(library(csv)).   21:- use_module(library(apply)).   22:- use_module(library(lists)).

TakeoffMetric US construction estimating constants (lookup only)

A constant is a dict with the keys id, category, material_or_item, quantity, value_text, unit, unit_si, condition, source, date_opened (a term date(Y,M,D)), used_by_tool_url, engine_file, engine_symbol (lists of strings), notes, withheld (true or false) and withheld_hosts (list of strings), plus value and value_si (floats). Text values are strings. source is a dict with the keys publisher, title, url and type.

When withheld is true the publisher does not allow republication: the dict has no value and no value_si key, value_text is the empty string, and the value is read at the URL of the source. value_si is also absent when the table leaves it empty.

 all(-Constants:list(dict)) is det
Every constant, in table order.
   44all(Constants) :-
   45    findall(C, constant_(_, C), Constants).
 by_id(+Id, -Constant:dict) is semidet
The constant with the given id (atom or string), for example 'CON-001'. Fails when the id is unknown.
   52by_id(Id, Constant) :-
   53    text_to_string(Id, S),
   54    constant_(S, C),
   55    !,
   56    Constant = C.
 search(+Query, -Constants:list(dict)) is det
The constants whose id, category, material, quantity, condition, publisher or notes contain every word of Query, ignoring case. An empty query returns the empty list.
   64search(Query, Constants) :-
   65    text_to_string(Query, Q0),
   66    string_lower(Q0, Q),
   67    split_string(Q, " \t\n\r\v\f", " \t\n\r\v\f", Parts),
   68    exclude(==(""), Parts, Words),
   69    (   Words == []
   70    ->  Constants = []
   71    ;   findall(C, (constant_(_, C), matches(Words, C)), Constants)
   72    ).
   73
   74matches(Words, C) :-
   75    atomics_to_string([C.id, " ", C.category, " ", C.material_or_item, " ",
   76                       C.quantity, " ", C.condition, " ", C.source.publisher,
   77                       " ", C.notes], Text0),
   78    string_lower(Text0, Text),
   79    forall(member(W, Words), sub_string(Text, _, _, _, W)).
 sources(-Sources:list(dict)) is det
Each distinct source of the table once, sorted by publisher, then title, then URL (code order, as the byte order of the Go module for this table).
   86sources(Sources) :-
   87    findall(k(P, T, U, Ty), ( constant_(_, C),
   88                              _{publisher:P, title:T, url:U, type:Ty} :< C.source
   89                            ), Keys),
   90    sort(Keys, Sorted),
   91    maplist(key_source, Sorted, Sources).
   92
   93key_source(k(P, T, U, Ty), _{publisher:P, title:T, url:U, type:Ty}).
   94
   95% Loading: the table is read when this file is compiled and stored as
   96% constant_(Id, Dict) facts, in table order.
   97
   98header([id, category, material_or_item, quantity, value, unit, value_si,
   99        unit_si, condition, source_publisher, source_title, source_url,
  100        source_type, date_opened, used_by_tool_url, engine_file,
  101        engine_symbol, notes, values_withheld, withheld_hosts]).
  102
  103table_clauses(File, Clauses) :-
  104    csv_read_file(File, [Header|Rows], [convert(false), strip(false)]),
  105    Header =.. [_|Names],
  106    (   header(Names)
  107    ->  true
  108    ;   throw(error(format("~w: unexpected header", [File]), _))
  109    ),
  110    foldl(row_clause, Rows, Clauses, 2, _).
  111
  112row_clause(Row, constant_(Id, C), Line0, Line) :-
  113    Line is Line0 + 1,
  114    Row =.. [_|Atoms],
  115    maplist(atom_string, Atoms, Fields),
  116    catch(parse(Fields, C),
  117          error(format(Fmt, Args), _),
  118          ( format(string(Msg), Fmt, Args),
  119            throw(error(format("data/constants.csv row ~w: ~w", [Line0, Msg]), _)) )),
  120    Id = C.id.
  121
  122parse(Fields, C) :-
  123    (   length(Fields, 20)
  124    ->  true
  125    ;   length(Fields, N), throw(error(format("~w fields, want 20", [N]), _))
  126    ),
  127    Fields = [Id, Category, Material, Quantity, Value, Unit, ValueSI, UnitSI,
  128              Condition, Publisher, Title, URL, Type, Date, UsedBy, EngineFile,
  129              EngineSymbol, Notes, Withheld0, Hosts],
  130    withheld(Withheld0, Value, ValueSI, Hosts, Withheld),
  131    date(Date, DateOpened),
  132    split_list(UsedBy, "; ", UsedByList),
  133    split_list(EngineFile, "; ", EngineFileList),
  134    split_list(EngineSymbol, "; ", EngineSymbolList),
  135    split_list(Hosts, ";", HostList),
  136    C0 = _{id:Id, category:Category, material_or_item:Material,
  137           quantity:Quantity, value_text:Value, unit:Unit, unit_si:UnitSI,
  138           condition:Condition,
  139           source:_{publisher:Publisher, title:Title, url:URL, type:Type},
  140           date_opened:DateOpened, used_by_tool_url:UsedByList,
  141           engine_file:EngineFileList, engine_symbol:EngineSymbolList,
  142           notes:Notes, withheld:Withheld, withheld_hosts:HostList},
  143    optional_number(Withheld, value, Value, C0, C1),
  144    optional_number(false, value_si, ValueSI, C1, C).
  145
  146withheld("true", Value, ValueSI, _, true) :-
  147    !,
  148    % A withheld row must carry no number at all.
  149    (   Value == "", ValueSI == ""
  150    ->  true
  151    ;   throw(error(format("withheld row has a value", []), _))
  152    ).
  153withheld("false", Value, _, Hosts, false) :-
  154    !,
  155    (   Hosts \== ""
  156    ->  throw(error(format("withheld_hosts set on a row that is not withheld", []), _))
  157    ;   Value == ""
  158    ->  throw(error(format("value missing on a row that is not withheld", []), _))
  159    ;   true
  160    ).
  161withheld(Other, _, _, _, _) :-
  162    throw(error(format("values_withheld: \"~w\", want true or false", [Other]), _)).
  163
  164optional_number(true, _, _, C, C) :- !.
  165optional_number(_, _, "", C, C) :- !.
  166optional_number(_, Key, Text, C0, C) :-
  167    (   catch(number_string(N, Text), _, fail), number(N)
  168    ->  F is float(N),
  169        put_dict(Key, C0, F, C)
  170    ;   throw(error(format("~w: \"~w\"", [Key, Text]), _))
  171    ).
  172
  173date(Text, date(Y, M, D)) :-
  174    (   split_string(Text, "-", "", [YS, MS, DS]),
  175        string_length(YS, 4), string_length(MS, 2), string_length(DS, 2),
  176        number_string(Y, YS), number_string(M, MS), number_string(D, DS),
  177        integer(Y), integer(M), integer(D),
  178        between(1, 12, M), D >= 1,
  179        days_in_month(Y, M, Max), D =< Max
  180    ->  true
  181    ;   throw(error(format("date_opened: \"~w\"", [Text]), _))
  182    ).
  183
  184days_in_month(Y, 2, D) :-
  185    !,
  186    (   ( Y mod 4 =:= 0, Y mod 100 =\= 0 ; Y mod 400 =:= 0 ) -> D = 29 ; D = 28 ).
  187days_in_month(_, M, D) :-
  188    nth1(M, [31, _, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], D).
  189
  190split_list("", _, []) :- !.
  191split_list(Text, Separator, Parts) :-
  192    atomic_list_concat(Atoms, Separator, Text),
  193    maplist(atom_string, Atoms, Parts).
  194
  195term_expansion(constant_table, Clauses) :-
  196    prolog_load_context(directory, Dir),
  197    atomic_list_concat([Dir, '/../data/constants.csv'], File0),
  198    absolute_file_name(File0, File, [access(read)]),
  199    table_clauses(File, Clauses).
  200
  201constant_table