omdl  v1.0
OpenSCAD Mechanical Design Library
map.scad
Go to the documentation of this file.
1 //! Map data structure and operations.
2 /***************************************************************************//**
3  \file
4  \author Roy Allen Sutton
5  \date 2015-2026
6 
7  \copyright
8 
9  This file is part of [omdl] (https://github.com/royasutton/omdl),
10  an OpenSCAD mechanical design library.
11 
12  The \em omdl is free software; you can redistribute it and/or modify
13  it under the terms of the [GNU Lesser General Public License]
14  (http://www.gnu.org/licenses/lgpl.html) as published by the Free
15  Software Foundation; either version 2.1 of the License, or (at
16  your option) any later version.
17 
18  The \em omdl is distributed in the hope that it will be useful,
19  but WITHOUT ANY WARRANTY; without even the implied warranty of
20  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21  Lesser General Public License for more details.
22 
23  You should have received a copy of the GNU Lesser General Public
24  License along with the \em omdl; if not, write to the Free Software
25  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26  02110-1301, USA; or see <http://www.gnu.org/licenses/>.
27 
28  \details
29 
30  \amu_define group_name (Maps)
31  \amu_define group_brief (Map data type and operations.)
32 
33  \amu_include (include/amu/doxyg_init_pd_gds_ipg.amu)
34 *******************************************************************************/
35 
36 // auto-tests (add to test results page)
37 /***************************************************************************//**
38  \amu_include (include/amu/validate_log.amu)
39  \amu_include (include/amu/validate_results.amu)
40 *******************************************************************************/
41 
42 // group(s) begin (test summary and includes-required)
43 /***************************************************************************//**
44  \amu_include (include/amu/doxyg_define_in_parent_open.amu)
45  \amu_include (include/amu/validate_summary.amu)
46  \amu_include (include/amu/includes_required.amu)
47 *******************************************************************************/
48 
49 // member-wide reference definitions
50 /***************************************************************************//**
51  \amu_define group_references
52  (
53  )
54 *******************************************************************************/
55 
56 // member-wide documentation and conventions
57 /***************************************************************************//**
58  \addtogroup \amu_eval(${group})
59  \details
60  \anchor \amu_eval(${group})_conventions
61  \par Conventions
62 
63  - A map is a \b list of \b [key, value] pairs. Keys must be \b strings.
64  - The map variable is always named \p m; a second map is \p m2.
65  - \c map_merge(m1, m2): when both maps contain the same key, the value
66  from \p m1 takes precedence. Order of keys in the result is unspecified.
67  - \c map_update(m, u): keys in \p u that are also in \p m update the
68  existing value; keys in \p u absent from \p m are appended.
69  Values in \p m absent from \p u are preserved unchanged.
70  - The special variable \c $map_strict controls whether map_get_value()
71  asserts on a missing key (strict=true) or returns \b undef silently
72  (strict=false, the default).
73  - map_errors() returns an empty list when the map is valid; a non-empty
74  list indicates the detected error class(es).
75 
76  \b Example
77 
78  \amu_define title (Map use)
79  \amu_define scope_id (example_use)
80  \amu_include (include/amu/scope.amu)
81 *******************************************************************************/
82 
83 //----------------------------------------------------------------------------//
84 // members
85 //----------------------------------------------------------------------------//
86 
87 //----------------------------------------------------------------------------//
88 // global configuration variables
89 //----------------------------------------------------------------------------//
90 
91 //! \name Variables
92 //! @{
93 
94 //! <boolean> Enforce strict checking for map value references.
95 $map_strict = false;
96 
97 //! @}
98 
99 //----------------------------------------------------------------------------//
100 // functions and modules
101 //----------------------------------------------------------------------------//
102 
103 //! \name Functions
104 //! @{
105 
106 //! Return the index of a map key.
107 /***************************************************************************//**
108  \param m <map> A list of N key-value map pairs.
109  \param k <value> A map key.
110 
111  \returns <integer> The index of the map entry if it exists.
112  Returns \b undef if \p key does not exist.
113 *******************************************************************************/
114 function map_get_index
115 (
116  m,
117  k
118 ) = let(i = first(search([k], m, 1, 0 )))
119  (i == empty_lst) ? undef : i;
120 
121 //! Test if a key exists.
122 /***************************************************************************//**
123  \param m <map> A list of N key-value map pairs.
124  \param k <value> A map key.
125 
126  \returns <boolean> \b true when the key exists and \b false otherwise.
127 *******************************************************************************/
128 function map_exists
129 (
130  m,
131  k
132 ) = is_defined(map_get_index(m, k));
133 
134 //! Get the map value associated with a key.
135 /***************************************************************************//**
136  \param m <map> A list of N key-value map pairs.
137  \param k <value> A map key.
138 
139  \returns <value> The value associated with \p key.
140  Returns \b undef if \p key does not exist and
141  \b $map_strict is \b false. Raises an assertion if
142  \p key does not exist and \b $map_strict is \b true.
143 *******************************************************************************/
144 function map_get_value
145 (
146  m,
147  k
148 ) = let( key_check = ! $map_strict || map_exists(m, k) )
149  assert( key_check, strl(["map key missing [", k, "]."]) )
150  second(m[map_get_index(m, k)]);
151 
152 //! Get a list of all map keys.
153 /***************************************************************************//**
154  \param m <map> A list of N key-value map pairs.
155 
156  \returns <list-N> A list of keys for all N map entries.
157 *******************************************************************************/
158 function map_get_keys
159 (
160  m
161 ) = select_e(m, f=true);
162 
163 //! Get a list of all map values.
164 /***************************************************************************//**
165  \param m <map> A list of N key-value map pairs.
166 
167  \returns <list-N> A list of values for all N map entries.
168 *******************************************************************************/
169 function map_get_values
170 (
171  m
172 ) = select_e(m, l=true);
173 
174 //! Get the the first value associated with an existing key in one of two maps.
175 /***************************************************************************//**
176  \param m1 <map> A list of N key-value map pairs.
177  \param m2 <map> A list of N key-value map pairs.
178  \param k <value> A map key.
179  \param d <value> A default return value.
180 
181  \returns <value> The first value associated with \p key that exists
182  in maps \p m1 or \p m2, otherwise return \p d when it does
183  not exist in either.
184 *******************************************************************************/
185 function map_get_firstof2_or
186 (
187  m1,
188  m2,
189  k,
190  d
191 ) = map_exists(m1, k) ? map_get_value(m1, k)
192  : map_exists(m2, k) ? map_get_value(m2, k)
193  : d;
194 
195 //! Get the number of map entries.
196 /***************************************************************************//**
197  \param m <map> A list of N key-value map pairs.
198 
199  \returns <integer> The number of map entries.
200 *******************************************************************************/
201 function map_get_size
202 (
203  m
204 ) = len(m);
205 
206 //! Merge the unique key-value pairs of a second map with those of a first.
207 /***************************************************************************//**
208  \param m1 <map> A list of N key-value map pairs.
209  \param m2 <map> A list of N key-value map pairs.
210 
211  \returns <value> The key value-pairs of \p m1 together with the
212  unique key value-pairs of \p m2 that are absent in \p m1.
213 *******************************************************************************/
214 function map_merge
215 (
216  m1,
217  m2
218 ) = [
219  // output all key-value pairs of 'm1'
220  for (k = map_get_keys(m1) )
221  [k, map_get_value(m1, k)],
222 
223  // output all key-value pairs of 'm2' not present in 'm1'
224  for (k = map_get_keys(m2) )
225  if ( !map_exists(m1, k) )
226  [k, map_get_value(m2, k)]
227  ];
228 
229 //! Update existing key-value pairs of a map.
230 /***************************************************************************//**
231  \param m <map> A list of N key-value map pairs.
232  \param u <map> The update list of N key-value map pairs.
233  \param ignore <boolean> Ignore the entries of \p u missing from \p m.
234 
235  \returns <value> The key value-pairs of \p m together with the
236  updates from \p u that are present in \p m.
237 *******************************************************************************/
238 function map_update
239 (
240  m,
241  u,
242  ignore = false
243 ) = let
244  (
245  mk = map_get_keys(m),
246  uk = map_get_keys(u),
247 
248  ak = [for (k = uk) if (!map_exists(m, k)) k],
249 
250  missing_keys = is_empty( ak ) || ignore
251  )
252  assert
253  (
254  missing_keys,
255  strl(["Update includes keys missing in map = ", ak])
256  )
257  [
258  for (k = map_get_keys(m) )
259  if ( map_exists(u, k) )
260  [k, map_get_value(u, k)]
261  else
262  [k, map_get_value(m, k)]
263  ];
264 
265 //! Compare the keys and/or values of two maps to test for equality.
266 /***************************************************************************//**
267  \param m1 <map> A list of N key-value map pairs.
268  \param m2 <map> A list of N key-value map pairs.
269  \param keys <boolean> Comparison includes the map keys.
270  \param values <boolean> Comparison includes the map values.
271  \param sort <boolean> Sort prior to the comparison.
272 
273  \returns <boolean> \b true when equal and \b false otherwise.
274 *******************************************************************************/
275 function map_equal(m1, m2, keys=true, values=false, sort=true) =
276  let
277  (
278  k = ! keys ? true
279  : let
280  (
281  k1 = map_get_keys(m1),
282  k2 = map_get_keys(m2),
283 
284  kc1 = sort ? sort_q2(k1) : k1,
285  kc2 = sort ? sort_q2(k2) : k2
286  )
287  (kc1 == kc2),
288 
289  v = ! values ? true
290  : let
291  (
292  v1 = map_get_values(m1),
293  v2 = map_get_values(m2),
294 
295  vc1 = sort ? sort_q2(v1) : v1,
296  vc2 = sort ? sort_q2(v2) : v2
297  )
298  (vc1 == vc2)
299  )
300  ( k && v );
301 
302 //! Create a map from two selected columns of a data table.
303 /***************************************************************************//**
304  \param t <table> A 2d data matrix.
305  \param keys <integer> The table column for the map keys.
306  \param values <integer> The table column for the map values.
307 
308  \returns <map> A list of N key-value map pairs.
309 *******************************************************************************/
310 function map_from_table(t, keys=0, values=1) =
311  let
312  (
313  k = select_e (t, keys),
314  v = select_e (t, values)
315  )
316  merge_p([k, v], j=true);
317 
318 //! Create a table from a list of maps with common keys.
319 /***************************************************************************//**
320  \param ml <map-list> A list of one or more maps.
321  \param sort <boolean> Sort the output by key.
322 
323  \returns <table> The table row data matrix (C-columns x R-Rows),
324  where \p C is the number of maps and \p R is the number of
325  map keys.
326 *******************************************************************************/
327 function map_to_table
328 (
329  ml,
330  sort = false
331 ) = let
332  (
333  // first map
334  m0 = first( ml ),
335 
336  // first map keys
337  k0 = map_get_keys( m0 ),
338 
339  // vector of map sizes
340  sv = [for (m = ml) map_get_size(m)],
341 
342  // vector of map key differences
343  kv = [for (m = ml) not_common(k0, map_get_keys(m))]
344  )
345  assert
346  ( // all map sizes must be equal
347  all_equal( cv = map_get_size(m0), v = sv ),
348  strl([ "All maps must be of equal size; sv=[", sv, "]." ])
349  )
350  assert
351  ( // all map must have same keys (all keys must be in common)
352  all_equal( cv = empty_lst, v = kv ),
353  strl([ "All maps must have same keys; kv=[", kv, "]." ])
354  )
355  [ // for each key
356  for (k = sort ? sort_q2( k0 ) : k0)
357  [ // output the key value of each map
358  k, for (m = ml) map_get_value(m, k)
359  ]
360  ];
361 
362 //! Perform basic format checks on a map and return errors.
363 /***************************************************************************//**
364  \param m <map> A list of N key-value map pairs.
365 
366  \returns <list-N> A list of map format errors.
367 
368  \details
369 
370  Check that: (1) each entry has key-value 2-tuple and (2) key
371  identifiers are unique. When there are no errors, the \b empty_lst
372  is returned.
373 *******************************************************************************/
374 function map_errors
375 (
376  m
377 ) =
378  let
379  (
380  // (1) each entry has key-value 2-tuple.
381  ec1 =
382  [
383  for ( i = [0:map_get_size(m)-1] )
384  let ( entry = m[i], key = first(entry) )
385  if ( 2 != len(entry) )
386  str
387  (
388  "map index ", i,
389  ", entry=", entry,
390  ", has incorrect count=[", len(entry),"]"
391  )
392  ],
393 
394  // (2) no repeat key identifiers.
395  ec2 =
396  [
397  for ( i = [0:map_get_size(m)-1] )
398  let ( entry = m[i], key = first(entry) )
399  if ( len(first(search([key], m, 0, 0))) > 1 )
400  str
401  (
402  "map index ", i,
403  ", key=[", key,"] not unique."
404  )
405  ]
406  )
407  concat(ec1, ec2);
408 
409 //! Perform basic format checks on a map and output errors to console.
410 /***************************************************************************//**
411  \param m <map> A list of N key-value map pairs.
412 
413  \param verbose <boolean> Be verbose during check.
414 
415  \details
416 
417  Check that: (1) each entry has key-value 2-tuple and (2) key
418  identifiers are unique.
419 *******************************************************************************/
420 module map_check
421 (
422  m,
423  verbose = false
424 )
425 {
426  if (verbose) log_info("begin map check");
427 
428  if (verbose) log_info ("checking map format and keys.");
429 
430  if ( map_get_size(m) > 0 )
431  for ( i = [0:map_get_size(m)-1] )
432  {
433  entry = m[i];
434  key = first(entry);
435 
436  // (1) each entry has key-value 2-tuple.
437  assert
438  (
439  len(entry) == 2,
440  str
441  (
442  "map index ", i,
443  ", entry=", entry,
444  ", has incorrect count=[", len(entry),"]"
445  )
446  );
447 
448  // (2) no repeat key identifiers.
449  if ( len(first(search([key], m, 0, 0))) > 1 )
450  log_warn
451  (
452  str
453  (
454  "map index ", i,
455  ", key=[", key,"] not unique."
456  )
457  );
458  }
459 
460  if (verbose)
461  {
463  (
464  str
465  (
466  "map size: ",
467  map_get_size(m), " entries."
468  )
469  );
470 
471  log_info("end map check");
472  }
473 }
474 
475 //! Dump each map entry to the console.
476 /***************************************************************************//**
477  \param m <map> A list of N key-value map pairs.
478  \param sort <boolean> Sort the output by key.
479  \param number <boolean> Output index number.
480  \param align <boolean> pad keys for right alignment.
481  \param p <integer> Number of places for zero-padded numbering.
482 *******************************************************************************/
483 module map_dump
484 (
485  m,
486  sort = false,
487  number = true,
488  align = true,
489  p = 3
490 )
491 {
492  if ( map_get_size(m) > 0 )
493  {
494  keys = map_get_keys(m);
495 
496  // calculate max key field length when aligning
497  maxl = align ?
498  max ( [ for (i = keys) is_string(i) ? len(i) : len( strl([i]) ) ] )
499  : 0;
500 
501  for (k = (sort == true) ? sort_q(keys) : keys)
502  {
503  // numbering with prefixed zero-padding
504  num = let
505  (
506  i = map_get_index(m, k),
507  z = chr( consts(p - len(str(i)), 48) )
508  )
509  number ?
510  str(z, i, ": ")
512 
513  // right align key with space padding
514  pad = let
515  (
516  s = is_string(k) ? len(k) : len( strl([k]) )
517  )
518  align ?
519  chr( consts(maxl - s, 32) )
520  : empty_str;
521 
522  val = map_get_value(m, k);
523 
524  log_echo ( str( num, pad, "'", k, "' = ", "'", val, "'" ) );
525  }
526  }
527 
528  if ( number )
529  log_echo(str("map size: ", map_get_size(m), " entries."));
530 }
531 
532 //! Write formatted map entries to the console.
533 /***************************************************************************//**
534  \param m <map> A list of N key-value map pairs.
535  \param ks <string-list> A list of selected keys.
536  \param sort <boolean> Sort the output by key.
537  \param number <boolean> Output index number.
538  \param fs <string> A field separator.
539  \param thn <string> Column heading for numbered row output.
540  \param index_tags <string-list> List of html formatting tags.
541  \param key_tags <string-list> List of html formatting tags.
542  \param value_tags <string-list> List of html formatting tags.
543 
544  \details
545 
546  Output map keys and values the console. To output only select keys,
547  assign the desired key identifiers to \p ks. For example to output
548  only 'key1' and 'key2', assign <tt>ks = ["key1", "key2"]</tt>. The
549  output can then be processed to produce documentation tables as
550  shown in the example below.
551 
552  \amu_define title (Map write)
553  \amu_define scope_id (example_table)
554  \amu_include (include/amu/scope_table.amu)
555 *******************************************************************************/
556 module map_write
557 (
558  m,
559  ks,
560  sort = false,
561  number = false,
562  fs = "^",
563  thn = "idx",
564  index_tags = empty_lst,
565  key_tags = ["b"],
566  value_tags = empty_lst
567 )
568 {
569  if ( map_get_size(m) > 0 )
570  {
571  num = number ? str(thn, fs) : empty_str;
572 
573  // map heading
574  log_echo ( str ( num, "key", fs, "value" ) );
575 
576  // map data
577  keys = map_get_keys(m);
578 
579  for (k = (sort == true) ? sort_q(keys) : keys)
580  {
581  if
582  (
583  is_undef( ks ) ||
584  is_number( first( search( [k], ks, 1, 0 ) ) )
585  )
586  {
587  num = let
588  (
589  i = map_get_index(m, k)
590  )
591  number ?
592  str( strl_html([i], p=[index_tags]), fs )
593  : empty_str;
594 
595  key = strl_html([k], p=[key_tags]);
596 
597  val = strl_html([map_get_value(m, k)], p=[value_tags]);
598 
599  log_echo ( str ( num, key, fs, val, fs ) );
600  }
601  }
602  }
603 }
604 
605 //! @}
606 
607 //! @}
608 //! @}
609 
610 //----------------------------------------------------------------------------//
611 // openscad-amu auxiliary scripts
612 //----------------------------------------------------------------------------//
613 
614 /*
615 BEGIN_SCOPE validate;
616  BEGIN_OPENSCAD;
617  include <omdl-base.scad>;
618  include <common/validation.scad>;
619 
620  echo( str("openscad version ", version()) );
621  for (i=[1:13]) echo( "not tested:" );
622 
623  // end_include
624  END_OPENSCAD;
625 
626  BEGIN_MFSCRIPT;
627  include --path "${INCLUDE_PATH}" {var_init,var_gen_term}.mfs;
628  include --path "${INCLUDE_PATH}" scr_make_mf.mfs;
629  END_MFSCRIPT;
630 END_SCOPE;
631 */
632 
633 /*
634 BEGIN_SCOPE example_use;
635  BEGIN_OPENSCAD;
636  include <omdl-base.scad>;
637 
638  map =
639  [
640  ["part1", ["screw10", [10, 11, 13]]],
641  ["part2", ["screw12", [20, 21, 30]]],
642  ["part3", ["screw10", [10, 10, -12]]],
643  ["config", ["top", "front", "rear"]],
644  ["version", [21, 5, 0]],
645  ["runid", 10]
646  ];
647 
648  echo( "### map_check ###" );
649  map_check(map, true);
650 
651  echo( "### map_exists ###" );
652  echo( str("is part0 = ", map_exists(map, "part0")) );
653  echo( str("is part1 = ", map_exists(map, "part1")) );
654 
655  echo( "### map_get_value ###" );
656  p1 = map_get_value(map, "part1");
657  echo( c=second(p1) );
658 
659  keys = map_get_keys(map);
660  parts = delete(keys, mv=["config", "version", "runid"]);
661 
662  echo( "### map_delete ###" );
663  for ( p = parts )
664  echo
665  (
666  n=p,
667  p=first(map_get_value(map, p)),
668  l=second(map_get_value(map, p))
669  );
670 
671  echo( "### map_dump ###" );
672  map_dump(map);
673 
674  // end_include
675  END_OPENSCAD;
676 
677  BEGIN_MFSCRIPT;
678  include --path "${INCLUDE_PATH}" {var_init,var_gen_term}.mfs;
679  include --path "${INCLUDE_PATH}" scr_make_mf.mfs;
680  END_MFSCRIPT;
681 END_SCOPE;
682 */
683 
684 /*
685 BEGIN_SCOPE example_table;
686  BEGIN_OPENSCAD;
687  include <omdl-base.scad>;
688 
689  map =
690  [
691  ["part1", ["screw10", [10, 11, 13]]],
692  ["part2", ["screw12", [20, 21, 30]]],
693  ["part3", ["screw10", [10, 10, -12]]],
694  ["config", ["top", "front", "rear"]],
695  ["version", [21, 5, 0]],
696  ["runid", 10]
697  ];
698 
699  map_write(map, index_tags=["center","i"]);
700 
701  // end_include
702  END_OPENSCAD;
703 
704  BEGIN_MFSCRIPT;
705  include --path "${INCLUDE_PATH}" {var_init,var_gen_term}.mfs;
706  include --path "${INCLUDE_PATH}" scr_make_mf.mfs;
707  END_MFSCRIPT;
708 END_SCOPE;
709 */
710 
711 //----------------------------------------------------------------------------//
712 // end of file
713 //----------------------------------------------------------------------------//
module log_warn(m)
Output warning message to console.
Definition: console.scad:333
module log_echo(m)
Output message to console.
Definition: console.scad:272
module log_info(m)
Output information message to console.
Definition: console.scad:318
empty_str
<string> A string with no characters (the empty string).
Definition: constants.scad:301
empty_lst
<list> A list with no values (the empty list).
Definition: constants.scad:304
function unique(v)
Return a list of the unique elements of an iterable value.
function count(mv, v, s=true, i)
Count all occurrences of a match value in an iterable value.
function not_common(v1, v2)
Return a list of the elements not present in both iterable values.
function second(v)
Return the second element of an iterable value.
function first(v)
Return the first element of an iterable value.
function all_equal(v, cv)
Test if all elements of an iterable value equal a comparison value.
function is_empty(v)
Test if an iterable value is empty.
function sort_q2(v, i, d=0, r=false, s=true)
Sort the elements of an iterable value using quick sort and compare.
function sort_q(v, i, r=false)
Sort the elements of an iterable value using quick sort.
function consts(l, v, u=false)
Create a list of constant or incrementing elements.
function strl(v)
Convert a list of values to a concatenated string.
function strl_html(v, b, p, a, f, d=false)
Convert a list of values to a concatenated HTML-formatted string.
function merge_p(v, j=true)
Parallel-merge the iterable elements of a list.
function select_e(v, i, f, l)
Select each element at an index position of a list of iterable values.
function map_equal(m1, m2, keys=true, values=false, sort=true)
Compare the keys and/or values of two maps to test for equality.
function map_update(m, u, ignore=false)
Update existing key-value pairs of a map.
function map_get_value(m, k)
Get the map value associated with a key.
module map_write(m, ks, sort=false, number=false, fs="^", thn="idx", index_tags=empty_lst, key_tags=["b"], value_tags=empty_lst)
Write formatted map entries to the console.
Definition: map.scad:1033
function map_get_keys(m)
Get a list of all map keys.
module map_check(m, verbose=false)
Perform basic format checks on a map and output errors to console.
Definition: map.scad:788
function map_from_table(t, keys=0, values=1)
Create a map from two selected columns of a data table.
function map_get_index(m, k)
Return the index of a map key.
function map_get_values(m)
Get a list of all map values.
function map_get_firstof2_or(m1, m2, k, d)
Get the the first value associated with an existing key in one of two maps.
module map_dump(m, sort=false, number=true, align=true, p=3)
Dump each map entry to the console.
Definition: map.scad:851
function map_merge(m1, m2)
Merge the unique key-value pairs of a second map with those of a first.
$map_strict
<boolean> Enforce strict checking for map value references.
Definition: map.scad:462
function map_exists(m, k)
Test if a key exists.
function map_get_size(m)
Get the number of map entries.
function map_errors(m)
Perform basic format checks on a map and return errors.
function map_to_table(ml, sort=false)
Create a table from a list of maps with common keys.
function is_defined(v)
Test if a value is defined.
function is_number(v)
Test if a value is a number.