data.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { escapeHtml, isString } from "./utilities";
  2. import get from "lodash/get";
  3. /**
  4. * Simple way to retrieve linked data in `value` param from task
  5. * Works only for prefixed values ($image); non-prefixed values left as is
  6. * It's possible to add some text which will be left untouched; that's useful for
  7. * visual Text tags to display some additional info ("Title: $title")
  8. * @param {string} value param
  9. * @param {object} task
  10. */
  11. export const parseValue = (value, task) => {
  12. const reVar = /\$[\w[\].{}]+/gi;
  13. if (!value) return "";
  14. // value can refer to structures, not only texts, so just replace wouldn't be enough
  15. if (value.match(reVar)?.[0] === value) {
  16. return get(task, value.slice(1)) ?? "";
  17. }
  18. return value.replace(reVar, (v) => get(task, v.slice(1) ?? ""));
  19. };
  20. /**
  21. * Parse CSV
  22. * Accepts only numbers as a data
  23. * Returns hash with names (or indexed hash for headless csv) as a keys
  24. * and arrays of numbers as a values
  25. * @param {string} text
  26. * @returns {{ [string]: number[] }}
  27. */
  28. export const parseCSV = (text, separator = "auto") => {
  29. // @todo iterate over newlines for better performance
  30. const lines = text.split("\n");
  31. let names;
  32. if (separator !== "auto" && !lines[0].includes(separator)) {
  33. throw new Error([`Cannot find provided separator "${separator}".`, `Row 1: ${lines[0]}`].join("\n"));
  34. }
  35. // detect separator (2nd line is definitely with data)
  36. if (separator === "auto" && lines.length > 1) {
  37. const candidates = lines[1].trim().match(/[,;\s\t]/g);
  38. if (!candidates.length) throw new Error("No separators found");
  39. if (candidates.some((c) => c !== candidates[0])) {
  40. const list = Array.from(new Set(candidates))
  41. .map(escapeHtml)
  42. .map((s) => `"${s}"`)
  43. .join(", ");
  44. throw new Error(
  45. [
  46. `More than one possible separator found: ${list}`,
  47. 'You can provide correct one with <Timeseries sep=",">',
  48. ].join("\n"),
  49. );
  50. }
  51. separator = candidates[0];
  52. if (lines[0].split(separator).length !== lines[1].split(separator).length)
  53. throw new Error(
  54. [
  55. "Different amount of elements in rows.",
  56. `Row 1: ${lines[0]}`,
  57. `Row 2: ${lines[1]}`,
  58. `Guessed separator: ${separator}`,
  59. 'You can provide correct one with <Timeseries sep=",">',
  60. ].join("\n"),
  61. );
  62. }
  63. const re = new RegExp(
  64. [
  65. '"(?:""|[^"])*"', // quoted text with possible quoted quotes inside it ("not a ""value""")
  66. `[^"${separator}]+`, // usual value, no quotes, between separators
  67. `(?=${separator}(?:${separator}|$))`, // empty value in the middle or at the end of string
  68. `^(?=${separator})`, // empty value at the start of the string
  69. ].join("|"),
  70. "g",
  71. );
  72. const split = (text) => text.trim().match(re);
  73. // detect header; if it is omitted, use indices as a header names
  74. names = split(lines[0]);
  75. const secondLine = split(lines[1]);
  76. // assume that we have at least one column with numbers
  77. // and name of this column is not number :)
  78. // so we have different types for values in first and second rows
  79. if (!names.every((n, i) => isNaN(n) === isNaN(secondLine[i]))) {
  80. lines.shift();
  81. names = names.map((n) => n.toLowerCase());
  82. } else {
  83. names = names.map((_, i) => String(i));
  84. }
  85. const result = {};
  86. for (const name of names) result[name] = [];
  87. if (names.length !== split(lines[0]).length) {
  88. throw new Error(
  89. [
  90. "Column names count differs from data columns count.",
  91. `Columns: ${names.join(", ")};`,
  92. `Data: ${lines[0]};`,
  93. `Separator: "${separator}".`,
  94. ].join("\n"),
  95. );
  96. }
  97. let row;
  98. let i;
  99. for (const line of lines) {
  100. // skip empty lines including the last line
  101. if (!line.trim()) continue;
  102. row = split(line);
  103. for (i = 0; i < row.length; i++) {
  104. const val = +row[i];
  105. result[names[i]].push(isNaN(val) ? row[i] : val);
  106. }
  107. }
  108. return [result, names];
  109. };
  110. /**
  111. * Internal helper to check if string is JSON
  112. * @param {string} value
  113. * @returns {object|false}
  114. */
  115. export const tryToParseJSON = (value) => {
  116. if (isString(value) && value[0] === "{") {
  117. try {
  118. return JSON.parse(value);
  119. } catch (e) {
  120. // somthing went wrong
  121. }
  122. }
  123. return false;
  124. };
  125. /**
  126. * Parse value type
  127. * Accept value type as a parameter
  128. * Returns type, seperator and options object by analyzing valueType
  129. */
  130. export const parseTypeAndOption = (valueType) => {
  131. const [, type, sep] = valueType.match(/^(\w+)(.)?/) ?? [];
  132. const options = {};
  133. if (sep) {
  134. const pairs = valueType.split(sep).slice(1);
  135. pairs.forEach((pair) => {
  136. const [k, v] = pair.split("=", 2);
  137. options[k] = v ?? true; // options without values are `true`
  138. });
  139. }
  140. return { type, sep, options };
  141. };