Guest User

importJSON

a guest
Jul 31st, 2019
118
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.57 KB | None | 0 0
  1. /*====================================================================================================================================*
  2. ImportJSON by Trevor Lohrbeer (@FastFedora)
  3. ====================================================================================================================================
  4. Version: 1.1
  5. Project Page: http://blog.fastfedora.com/projects/import-json
  6. Copyright: (c) 2012 by Trevor Lohrbeer
  7. License: GNU General Public License, version 3 (GPL-3.0)
  8. http://www.opensource.org/licenses/gpl-3.0.html
  9. ------------------------------------------------------------------------------------------------------------------------------------
  10. A library for importing JSON feeds into Google spreadsheets. Functions include:
  11.  
  12. ImportJSON For use by end users to import a JSON feed from a URL
  13. ImportJSONAdvanced For use by script developers to easily extend the functionality of this library
  14.  
  15. Future enhancements may include:
  16.  
  17. - Support for a real XPath like syntax similar to ImportXML for the query parameter
  18. - Support for OAuth authenticated APIs
  19.  
  20. Or feel free to write these and add on to the library yourself!
  21. ------------------------------------------------------------------------------------------------------------------------------------
  22. Changelog:
  23.  
  24. 1.1 Added support for the noHeaders option
  25. 1.0 Initial release
  26. *====================================================================================================================================*/
  27. /**
  28. * Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
  29. * a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
  30. * the JSON feed. The remaining rows contain the data.
  31. *
  32. * By default, data gets transformed so it looks more like a normal data import. Specifically:
  33. *
  34. * - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
  35. * of the rows representing their parent elements.
  36. * - Values longer than 256 characters get truncated.
  37. * - Headers have slashes converted to spaces, common prefixes removed and the resulting text converted to title case.
  38. *
  39. * To change this behavior, pass in one of these values in the options parameter:
  40. *
  41. * noInherit: Don't inherit values from parent elements
  42. * noTruncate: Don't truncate values
  43. * rawHeaders: Don't prettify headers
  44. * noHeaders: Don't include headers, only the data
  45. * debugLocation: Prepend each value with the row & column it belongs in
  46. *
  47. * For example:
  48. *
  49. * =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json", "/feed/entry/title,/feed/entry/content",
  50. * "noInherit,noTruncate,rawHeaders")
  51. *
  52. * @param {url} the URL to a public JSON feed
  53. * @param {query} a comma-separated lists of paths to import. Any path starting with one of these paths gets imported.
  54. * @param {options} a comma-separated list of options that alter processing of the data
  55. *
  56. * @return a two-dimensional array containing the data, with the first row containing headers
  57. * @customfunction
  58. **/
  59. function ImportJSON(url, query, options) {
  60. return ImportJSONAdvanced(url, query, options, includeXPath_, defaultTransform_);
  61. }
  62.  
  63. /**
  64. * An advanced version of ImportJSON designed to be easily extended by a script. This version cannot be called from within a
  65. * spreadsheet.
  66. *
  67. * Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
  68. * a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
  69. * the JSON feed. The remaining rows contain the data.
  70. *
  71. * Use the include and transformation functions to determine what to include in the import and how to transform the data after it is
  72. * imported.
  73. *
  74. * For example:
  75. *
  76. * =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json",
  77. * "/feed/entry",
  78. * function (query, path) { return path.indexOf(query) == 0; },
  79. * function (data, row, column) { data[row][column] = data[row][column].toString().substr(0, 100); } )
  80. *
  81. * In this example, the import function checks to see if the path to the data being imported starts with the query. The transform
  82. * function takes the data and truncates it. For more robust versions of these functions, see the internal code of this library.
  83. *
  84. * @param {url} the URL to a public JSON feed
  85. * @param {query} the query passed to the include function
  86. * @param {options} a comma-separated list of options that may alter processing of the data
  87. * @param {includeFunc} a function with the signature func(query, path, options) that returns true if the data element at the given path
  88. * should be included or false otherwise.
  89. * @param {transformFunc} a function with the signature func(data, row, column, options) where data is a 2-dimensional array of the data
  90. * and row & column are the current row and column being processed. Any return value is ignored. Note that row 0
  91. * contains the headers for the data, so test for row==0 to process headers only.
  92. *
  93. * @return a two-dimensional array containing the data, with the first row containing headers
  94. **/
  95. function ImportJSONAdvanced(url, query, options, includeFunc, transformFunc) {
  96. var jsondata = UrlFetchApp.fetch(url);
  97. var object = JSON.parse(jsondata.getContentText());
  98.  
  99. return parseJSONObject_(object, query, options, includeFunc, transformFunc);
  100. }
  101.  
  102. /**
  103. * Encodes the given value to use within a URL.
  104. *
  105. * @param {value} the value to be encoded
  106. *
  107. * @return the value encoded using URL percent-encoding
  108. */
  109. function URLEncode(value) {
  110. return encodeURIComponent(value.toString());
  111. }
  112.  
  113. /**
  114. * Parses a JSON object and returns a two-dimensional array containing the data of that object.
  115. */
  116. function parseJSONObject_(object, query, options, includeFunc, transformFunc) {
  117. var headers = new Array();
  118. var data = new Array();
  119.  
  120. if (query && !Array.isArray(query) && query.toString().indexOf(",") != -1) {
  121. query = query.toString().split(",");
  122. }
  123.  
  124. if (options) {
  125. options = options.toString().split(",");
  126. }
  127.  
  128. parseData_(headers, data, "", 1, object, query, options, includeFunc);
  129. parseHeaders_(headers, data);
  130. transformData_(data, options, transformFunc);
  131.  
  132. return hasOption_(options, "noHeaders") ? (data.length > 1 ? data.slice(1) : new Array()) : data;
  133. }
  134.  
  135. /**
  136. * Parses the data contained within the given value and inserts it into the data two-dimensional array starting at the rowIndex.
  137. * If the data is to be inserted into a new column, a new header is added to the headers array. The value can be an object,
  138. * array or scalar value.
  139. *
  140. * If the value is an object, it's properties are iterated through and passed back into this function with the name of each
  141. * property extending the path. For instance, if the object contains the property "entry" and the path passed in was "/feed",
  142. * this function is called with the value of the entry property and the path "/feed/entry".
  143. *
  144. * If the value is an array containing other arrays or objects, each element in the array is passed into this function with
  145. * the rowIndex incremeneted for each element.
  146. *
  147. * If the value is an array containing only scalar values, those values are joined together and inserted into the data array as
  148. * a single value.
  149. *
  150. * If the value is a scalar, the value is inserted directly into the data array.
  151. */
  152. function parseData_(headers, data, path, rowIndex, value, query, options, includeFunc) {
  153. var dataInserted = false;
  154.  
  155. if (isObject_(value)) {
  156. for (key in value) {
  157. if (parseData_(headers, data, path + "/" + key, rowIndex, value[key], query, options, includeFunc)) {
  158. dataInserted = true;
  159. }
  160. }
  161. } else if (Array.isArray(value) && isObjectArray_(value)) {
  162. for (var i = 0; i < value.length; i++) {
  163. if (parseData_(headers, data, path, rowIndex, value[i], query, options, includeFunc)) {
  164. dataInserted = true;
  165. rowIndex++;
  166. }
  167. }
  168. } else if (!includeFunc || includeFunc(query, path, options)) {
  169. // Handle arrays containing only scalar values
  170. if (Array.isArray(value)) {
  171. value = value.join();
  172. }
  173.  
  174. // Insert new row if one doesn't already exist
  175. if (!data[rowIndex]) {
  176. data[rowIndex] = new Array();
  177. }
  178.  
  179. // Add a new header if one doesn't exist
  180. if (!headers[path] && headers[path] != 0) {
  181. headers[path] = Object.keys(headers).length;
  182. }
  183.  
  184. // Insert the data
  185. data[rowIndex][headers[path]] = value;
  186. dataInserted = true;
  187. }
  188.  
  189. return dataInserted;
  190. }
  191.  
  192. /**
  193. * Parses the headers array and inserts it into the first row of the data array.
  194. */
  195. function parseHeaders_(headers, data) {
  196. data[0] = new Array();
  197.  
  198. for (key in headers) {
  199. data[0][headers[key]] = key;
  200. }
  201. }
  202.  
  203. /**
  204. * Applies the transform function for each element in the data array, going through each column of each row.
  205. */
  206. function transformData_(data, options, transformFunc) {
  207. for (var i = 0; i < data.length; i++) {
  208. for (var j = 0; j < data[i].length; j++) {
  209. transformFunc(data, i, j, options);
  210. }
  211. }
  212. }
  213.  
  214. /**
  215. * Returns true if the given test value is an object; false otherwise.
  216. */
  217. function isObject_(test) {
  218. return Object.prototype.toString.call(test) === '[object Object]';
  219. }
  220.  
  221. /**
  222. * Returns true if the given test value is an array containing at least one object; false otherwise.
  223. */
  224. function isObjectArray_(test) {
  225. for (var i = 0; i < test.length; i++) {
  226. if (isObject_(test[i])) {
  227. return true;
  228. }
  229. }
  230.  
  231. return false;
  232. }
  233.  
  234. /**
  235. * Returns true if the given query applies to the given path.
  236. */
  237. function includeXPath_(query, path, options) {
  238. if (!query) {
  239. return true;
  240. } else if (Array.isArray(query)) {
  241. for (var i = 0; i < query.length; i++) {
  242. if (applyXPathRule_(query[i], path, options)) {
  243. return true;
  244. }
  245. }
  246. } else {
  247. return applyXPathRule_(query, path, options);
  248. }
  249.  
  250. return false;
  251. };
  252.  
  253. /**
  254. * Returns true if the rule applies to the given path.
  255. */
  256. function applyXPathRule_(rule, path, options) {
  257. return path.indexOf(rule) == 0;
  258. }
  259.  
  260. /**
  261. * By default, this function transforms the value at the given row & column so it looks more like a normal data import. Specifically:
  262. *
  263. * - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
  264. * of the rows representing their parent elements.
  265. * - Values longer than 256 characters get truncated.
  266. * - Values in row 0 (headers) have slashes converted to spaces, common prefixes removed and the resulting text converted to title
  267. * case.
  268. *
  269. * To change this behavior, pass in one of these values in the options parameter:
  270. *
  271. * noInherit: Don't inherit values from parent elements
  272. * noTruncate: Don't truncate values
  273. * rawHeaders: Don't prettify headers
  274. * debugLocation: Prepend each value with the row & column it belongs in
  275. */
  276. function defaultTransform_(data, row, column, options) {
  277. if (!data[row][column]) {
  278. if (row < 2 || hasOption_(options, "noInherit")) {
  279. data[row][column] = "";
  280. } else {
  281. data[row][column] = data[row-1][column];
  282. }
  283. }
  284.  
  285. if (!hasOption_(options, "rawHeaders") && row == 0) {
  286. if (column == 0 && data[row].length > 1) {
  287. removeCommonPrefixes_(data, row);
  288. }
  289.  
  290. data[row][column] = toTitleCase_(data[row][column].toString().replace(/[\/\_]/g, " "));
  291. }
  292.  
  293. if (!hasOption_(options, "noTruncate") && data[row][column]) {
  294. data[row][column] = data[row][column].toString().substr(0, 256);
  295. }
  296.  
  297. if (hasOption_(options, "debugLocation")) {
  298. data[row][column] = "[" + row + "," + column + "]" + data[row][column];
  299. }
  300. }
  301.  
  302. /**
  303. * If all the values in the given row share the same prefix, remove that prefix.
  304. */
  305. function removeCommonPrefixes_(data, row) {
  306. var matchIndex = data[row][0].length;
  307.  
  308. for (var i = 1; i < data[row].length; i++) {
  309. matchIndex = findEqualityEndpoint_(data[row][i-1], data[row][i], matchIndex);
  310.  
  311. if (matchIndex == 0) {
  312. return;
  313. }
  314. }
  315.  
  316. for (var i = 0; i < data[row].length; i++) {
  317. data[row][i] = data[row][i].substring(matchIndex, data[row][i].length);
  318. }
  319. }
  320.  
  321. /**
  322. * Locates the index where the two strings values stop being equal, stopping automatically at the stopAt index.
  323. */
  324. function findEqualityEndpoint_(string1, string2, stopAt) {
  325. if (!string1 || !string2) {
  326. return -1;
  327. }
  328.  
  329. var maxEndpoint = Math.min(stopAt, string1.length, string2.length);
  330.  
  331. for (var i = 0; i < maxEndpoint; i++) {
  332. if (string1.charAt(i) != string2.charAt(i)) {
  333. return i;
  334. }
  335. }
  336.  
  337. return maxEndpoint;
  338. }
  339.  
  340.  
  341. /**
  342. * Converts the text to title case.
  343. */
  344. function toTitleCase_(text) {
  345. if (text == null) {
  346. return null;
  347. }
  348.  
  349. return text.replace(/\w\S*/g, function(word) { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); });
  350. }
  351.  
  352. /**
  353. * Returns true if the given set of options contains the given option.
  354. */
  355. function hasOption_(options, option) {
  356. return options && options.indexOf(option) >= 0;
  357. }
Advertisement
Add Comment
Please, Sign In to add comment