<?php function dump(string $string) { echo $string . PHP_EOL; } function write_data(array $data, string $filename) { \file_put_contents($filename, \json_encode($data)); } function array_clean(array $array) { $output = \array_unique($array); \sort($output); return $output; } function ask(string $prompt) { dump($prompt); $input = \rtrim(\fgets(STDIN)); return $input; } function find_missing_associations(array $mappingItems, array $categories, array $items) { $missing = []; foreach ($items as $item) { $set = \array_merge( $mappingItems[$item]['is'], $mappingItems[$item]['isnot'], $mappingItems[$item]['na'], ); foreach ($categories as $category) { if (!\in_array($category, $set)) { $missing[] = [ 'item' => $item, 'category' => $category, ]; } } } return $missing; } function find_exclusions(array $exclusions, string $searchedCategory) { $output = []; foreach ($exclusions as $exclusionSet) { if (\is_array($exclusionSet) && \in_array($searchedCategory, $exclusionSet)) { foreach ($exclusionSet as $candidate) { if ($candidate !== $searchedCategory) { $output[] = $candidate; } } } } return $output; } function clean_item_mappings(array $itemMappings) { $itemMappings['is'] = array_clean($itemMappings['is']); $itemMappings['isnot'] = array_clean($itemMappings['isnot']); $itemMappings['na'] = array_clean($itemMappings['na']); // remove duplicates $itemMappings['is'] = array_clean($itemMappings['is']); $tmpArray = []; foreach (array_clean($itemMappings['isnot']) as $itemIsNot) { if (!\in_array($itemIsNot, $itemMappings['is'])) { $tmpArray[] = $itemIsNot; } } $itemMappings['isnot'] = $tmpArray; $tmpArray = []; foreach (array_clean($itemMappings['na']) as $itemNa) { if (!\in_array($itemNa, $itemMappings['is']) && !\in_array($itemNa, $itemMappings['isnot'])) { $tmpArray[] = $itemNa; } } $itemMappings['na'] = $tmpArray; return $itemMappings; } if ($argc != 2) { dump('Need data json file as parameter.'); die; } $jsonDataFile = $argv[1]; $data = []; if (is_file($jsonDataFile)) { $data = \json_decode(\file_get_contents($jsonDataFile), true); } if (!is_writable($jsonDataFile)) { dump('Output data json file is not writable.'); die; } $categories = (\array_key_exists('categories', $data) && \is_array($data['categories'])) ? $data['categories'] : []; $items = (\array_key_exists('items', $data) && \is_array($data['items'])) ? $data['items'] : []; // Manage categories exclusions $exclusions = (\array_key_exists('exclusions', $data) && \is_array($data['exclusions'])) ? $data['exclusions'] : []; // Merge categories with categories found in exclusions foreach ($exclusions as $exclusionSet) { foreach ($exclusionSet as $category) { $categories[] = $category; } } $categories = array_clean($categories); $items = array_clean($items); $themes = (\array_key_exists('themes', $data) && \is_array($data['themes'])) ? $data['themes'] : []; $resources = (\array_key_exists('resources', $data) && \is_array($data['resources'])) ? $data['resources'] : []; $data['categories'] = $categories; $data['items'] = $items; $data['exclusions'] = $exclusions; $data['themes'] = $themes; $data['resources'] = $resources; dump(''); dump('Found ' . \count($categories) . ' unique categories.'); dump('Found ' . \count($items) . ' unique items.'); dump('Found ' . \count($exclusions) . ' exclusions sets.'); dump('Found ' . \count($themes) . ' themes.'); // Get/init mapping data $mapping = (\array_key_exists('mapping', $data) && \is_array($data['mapping'])) ? $data['mapping'] : []; $mappingItems = (\array_key_exists('items', $mapping) && \is_array($mapping['items'])) ? $mapping['items'] : []; foreach ($items as $item) { if (!\array_key_exists($item, $mappingItems)) { $mappingItems[$item] = []; } if (!\array_key_exists('is', $mappingItems[$item]) || !\is_array($mappingItems[$item])) { $mappingItems[$item]['is'] = []; } if (!\array_key_exists('isnot', $mappingItems[$item]) || !\is_array($mappingItems[$item])) { $mappingItems[$item]['isnot'] = []; } if (!\array_key_exists('na', $mappingItems[$item]) || !\is_array($mappingItems[$item])) { $mappingItems[$item]['na'] = []; } $mappingItems[$item] = clean_item_mappings($mappingItems[$item]); } // TODO: Should check/add unkown items from current mapping \ksort($mappingItems); $data['mapping'] = [ 'items' => $mappingItems, ]; function showCategories($categories) { dump(\join("\n", $categories)); } function showItems($items) { dump(\join("\n", $items)); } function showExclusions($exclusions) { foreach ($exclusions as $exclusionSet) { dump(\join(', ', $exclusionSet)); } } function showThemes($theme) { foreach ($theme as $name => $categories) { dump($name . ': ' . \join(', ', $categories)); } } function showMappings($mappingItems) { $columnsWidths = [ 'items' => 0, ]; $items = \array_keys($mappingItems); $categories = []; foreach ($mappingItems as $item => $mapping) { if ($columnsWidths['items'] < \mb_strlen($item)) { $columnsWidths['items'] = \mb_strlen($item); } foreach (\array_merge($mapping['is'], $mapping['isnot'], $mapping['na']) as $category) { if (!\in_array($category, $categories)) { $categories[] = $category; if (!\array_key_exists($category, $columnsWidths)) { $columnsWidths[$category] = 0; } if ($columnsWidths[$category] < \mb_strlen($category)) { $columnsWidths[$category] = \mb_strlen($category); } } } } $strIs = '✅'; $strIsNot = '❌'; $strNa = '⛔'; // separator $line = [ \mb_str_pad('', $columnsWidths['items'], '-', STR_PAD_BOTH), ]; foreach ($categories as $category) { $line[] = \mb_str_pad('', $columnsWidths[$category], '-', STR_PAD_BOTH); } dump('--' . \join('---', $line) . '--'); // header $line = [ \mb_str_pad('', $columnsWidths['items'], ' ', STR_PAD_BOTH), ]; foreach ($categories as $category) { $line[] = \mb_str_pad($category, $columnsWidths[$category], ' ', STR_PAD_BOTH); } dump('| ' . \join(' | ', $line) . ' |'); // separator $line = [ \mb_str_pad('', $columnsWidths['items'], '-', STR_PAD_BOTH), ]; foreach ($categories as $category) { $line[] = \mb_str_pad('', $columnsWidths[$category], '-', STR_PAD_BOTH); } dump('|-' . \join('-|-', $line) . '-|'); foreach ($items as $item) { $line = [ \mb_str_pad($item, $columnsWidths['items'], ' ', STR_PAD_RIGHT), ]; foreach ($categories as $category) { $value = ''; if (\in_array($category, $mappingItems[$item]['is'])) { $value = $strIs; } elseif (\in_array($category, $mappingItems[$item]['isnot'])) { $value = $strIsNot; } elseif (\in_array($category, $mappingItems[$item]['na'])) { $value = $strNa; } $line[] = \mb_str_pad($value, $columnsWidths[$category], ' ', STR_PAD_BOTH); } dump('| ' . \join(' | ', $line) . ' |'); } // separator $line = [ \mb_str_pad('', $columnsWidths['items'], '-', STR_PAD_BOTH), ]; foreach ($categories as $category) { $line[] = \mb_str_pad('', $columnsWidths[$category], '-', STR_PAD_BOTH); } dump('--' . \join('---', $line) . '--'); } function editMappings($mappingItems, $categories, $items, $exclusions) { // Set missing associations $exitEditMappings = false; $missing = find_missing_associations($mappingItems, $categories, $items); while ((\count($missing) !== 0) && ($exitEditMappings === false)) { dump(''); dump('Missing associations: ' . \count($missing)); dump(''); $picked = $missing[mt_rand(0, \count($missing) - 1)]; $item = $picked['item']; $category = $picked['category']; $question = 'Is "' . $item . '" can be categorised as "' . $category . '"?'; $ex = find_exclusions($exclusions, $category); if (\count($ex) !== 0) { $question .= ' (and apply accordingly to "' . join('" and "', $ex) . '")'; } dump($question); $answer = ask('1: yes ; 2: no ; 3: n/a ; 0: exit'); switch ($answer) { case '0': $exitEditMappings = true; break; case '1': dump(' -> "' . $item . '" is "' . $category . '"'); $mappingItems[$item]['is'][] = $category; // apply "is not" to each other foreach ($ex as $exclusion) { dump(' -> "' . $item . '" is not "' . $exclusion . '"'); $mappingItems[$item]['isnot'][] = $exclusion; } break; case '2': dump(' -> "' . $item . '" is not "' . $category . '"'); $mappingItems[$item]['isnot'][] = $category; // apply "is" only if one exclusion if (\count($ex) === 1) { foreach ($ex as $exclusion) { dump(' -> "' . $item . '" is "' . $exclusion . '"'); $mappingItems[$item]['is'][] = $exclusion; } } break; case '3': dump(' -> "' . $item . '" does not apply as "is or is not" "' . $category . '"'); $mappingItems[$item]['na'][] = $category; foreach ($ex as $excludedCategory) { dump(' -> "' . $item . '" does not apply as "is or is not" "' . $excludedCategory . '"'); $mappingItems[$item]['na'][] = $excludedCategory; } break; default: dump('wut? skipping...'); break; } $mappingItems[$item] = clean_item_mappings($mappingItems[$item]); $missing = find_missing_associations($mappingItems, $categories, $items); } return $mappingItems; } // Main loop $exitMainLoop = false; while ($exitMainLoop === false) { dump(''); $missing = find_missing_associations($mappingItems, $categories, $items); $menu = [ '0: save and exit', '', '1: show categories (' . \count($categories) . ' found)', '2: show items (' . \count($items) . ' found)', '3: show exclusions (' . \count($exclusions) . ' found)', '4: show themes (' . \count($themes) . ' found)', '5: show mappings (' . \count($mappingItems) . ' found)', '', '6: complete mappings (' . \count($missing) . ' missing)', ]; $answer = ask(\join("\n", $menu)); switch ($answer) { case '0': $exitMainLoop = true; break; case '1': showCategories($categories); break; case '2': showItems($items); break; case '3': showExclusions($exclusions); break; case '4': showThemes($themes); break; case '5': showMappings($mappingItems); break; case '6': $data['mapping']['items'] = editMappings($mappingItems, $categories, $items, $exclusions); break; default: break; } } write_data($data, $jsonDataFile); echo "ok, done." . PHP_EOL;