Advertisement
nicolaslagios

Import JSON to Google Sheets

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