Skip to content

Instantly share code, notes, and snippets.

@florentdescroix
Forked from pamelafox/exportjson.js
Last active July 3, 2024 00:44
Show Gist options
  • Save florentdescroix/9513cfd957f8b2cde7b832cf7170c84a to your computer and use it in GitHub Desktop.
Save florentdescroix/9513cfd957f8b2cde7b832cf7170c84a to your computer and use it in GitHub Desktop.
const EMPTY_VALUE = undefined;
// Includes functions for exporting active sheet or all sheets as JSON object (also Python object syntax compatible).
// Tweak the makePrettyJSON_ function to customize what kind of JSON to export.
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [
{name: "Export JSON for this sheet", functionName: "exportSheet"},
{name: "Export JSON for all sheets", functionName: "exportAllSheets"}
];
ss.addMenu("Export JSON", menuEntries);
}
function exportAllSheets() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sheetsData = {};
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
var rowsData = getRowsData_(sheet);
var sheetName = sheet.getName();
sheetsData[sheetName] = rowsData;
}
var json = makeJSON_(sheetsData);
displayText_(json);
return json;
}
function exportSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rowsData = getRowsData_(sheet);
var json = makeJSON_(rowsData);
displayText_(json);
}
function makeJSON_(object) {
var jsonString = JSON.stringify(object, null, 4);
return jsonString;
}
function displayText_(text) {
var output = HtmlService.createHtmlOutput("<textarea style='width:100%;' rows='20'>" + text + "</textarea>");
output.setWidth(400)
output.setHeight(300);
SpreadsheetApp.getUi()
.showModalDialog(output, 'Exported JSON');
}
/**
* getRowsData iterates row by row in the input range and returns an array of objects.
* Each object contains all the data for a given row, indexed by its normalized column name.
* Arguments:
* - sheet: the sheet object that contains the data to be processed
* - range: the exact range of cells where the data is stored
* - columnHeadersRowIndex: specifies the row number where the column names are stored.
* This argument is optional and it defaults to the row immediately above range;
* Returns an Array of objects.
*/
function getRowsData_(sheet) {
var headersRange = sheet.getRange(1, 1, sheet.getFrozenRows(), sheet.getMaxColumns());
var headers = headersRange.getValues()[0];
var dataRange = sheet.getRange(sheet.getFrozenRows()+1, 1, sheet.getLastRow(), headers.length);
var objects = getObjects_(dataRange.getValues(), dataRange.getRichTextValues(), headers);
return objects;
}
/**
* getColumnsData iterates column by column in the input range and returns an array of objects.
* Each object contains all the data for a given column, indexed by its normalized row name.
* Arguments:
* - sheet: the sheet object that contains the data to be processed
* - range: the exact range of cells where the data is stored
* - rowHeadersColumnIndex: specifies the column number where the row names are stored.
* This argument is optional and it defaults to the column immediately left of the range;
* Returns an Array of objects.
*/
function getColumnsData_(sheet, range, rowHeadersColumnIndex) {
rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;
var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getLastRow(), 1).getValues();
var headers = normalizeHeaders_(arrayTranspose_(headersTmp)[0]);
return getObjects_(arrayTranspose_(range.getValues()), headers);
}
/**
* For every row of data in data, generates an object that contains the data. Names of
* object fields are defined in keys.
* Arguments:
* - data: JavaScript 2d array
* - keys: Array of Strings that define the property names for the objects to create
*/
function getObjects_(data, richData, keys) {
var isObject = false;
var objects = [];
if (keys[0] === "id") {
objects = {};
isObject = true;
}
for (var i = 0; i < data.length; ++i) {
var id = false;
var object = {};
var hasData = keys.length;
for (var j = 0; j < data[i].length; ++j) {
var cellData = data[i][j];
if (keys[j].includes("[richText]")) {
cellData = htmlEncodeRichText_(richData[i][j]);
} else {
cellData = data[i][j];
}
if (isCellEmpty_(cellData)) {
hasData--;
}
if (isObject && keys[j] === "id") {
id = cellData;
} else {
setObjectData_(object, keys[j].replace("[richText]", ""), cellData);
}
}
if (hasData !== 0) {
if (isObject) {
if (id !== undefined) {
id = `${id}`;
let arr = id.split(".");
let obj = objects;
for (let i in arr) {
if (i == (arr.length - 1)) {
if (Object.keys(object).length === 1 && object.value) {
obj[arr[i]] = object.value;
} else {
obj[arr[i]] = object;
}
} else if (!(arr[i] in obj)) {
obj[arr[i]] = {};
}
obj = obj[arr[i]];
}
}
} else{
objects.push(object);
}
}
}
return objects;
}
/**
* For every key in the keys array
* recursively fill the object with data
* Arguments:
* - object: JavaScript object to fill
* - keys: Array (or '.' separated String) that deifine the imbricated properties names for the object to fill
* - data: value that should be put
*/
function setObjectData_(object, keys, data) {
if (!keys) return;
if (!Array.isArray(keys)) {
keys = keys.split(".");
}
if (keys.length == 1) {
if (keys[0].charAt(0) === "@") {
object.push({id: keys[0].substring(1), value: data});
} else if (!(Array.isArray(object) && data == "")) {
object[keys[0]] = data === "" ? EMPTY_VALUE : data;
}
} else {
if (!object.hasOwnProperty(keys[0])) {
if (!isNaN(keys[1]) || keys[1].charAt(0) === "@")
object[keys[0]] = [];
else
object[keys[0]] = {};
}
setObjectData_(object[keys[0]], keys.slice(1), data);
}
}
/**
* Returns an Array of normalized Strings.
* Arguments:
* - headers: Array of Strings to normalize
*/
function normalizeHeaders_(headers) {
var keys = [];
for (var i = 0; i < headers.length; ++i) {
var key = normalizeHeader_(headers[i]);
if (key.length > 0) {
keys.push(key);
}
}
return keys;
}
/**
* Normalizes a string, by removing all alphanumeric characters and using mixed case
* to separate words. The output will always start with a lower case letter.
* This function is designed to produce JavaScript object property names.
* Arguments:
* - header: string to normalize
* Examples:
* "First Name" -> "firstName"
* "Market Cap (millions) -> "marketCapMillions
* "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
* Keeps the "." for imbricated datas
*/
function normalizeHeader_(header) {
var key = "";
var upperCase = false;
for (var i = 0; i < header.length; ++i) {
var letter = header[i];
if (letter == " " && key.length > 0) {
upperCase = true;
continue;
}
if (letter !== "_" && letter !== "@" && letter !== "." && !isAlnum_(letter)) {
continue;
}
if (key.length == 0 && isDigit_(letter)) {
continue; // first character must be a letter
}
if (upperCase) {
upperCase = false;
key += letter.toUpperCase();
} else {
key += letter.toLowerCase();
}
}
return key;
}
/**
* Returns true if the cell where cellData was read from is empty.
* Arguments:
* - cellData: string
*/
function isCellEmpty_(cellData) {
return typeof(cellData) == "string" && cellData == "";
}
/**
* Returns true if the character char is alphabetical, false otherwise.
*/
function isAlnum_(char) {
return char >= 'A' && char <= 'Z' ||
char >= 'a' && char <= 'z' ||
isDigit_(char);
}
/**
* Returns true if the character char is a digit, false otherwise.
*/
function isDigit_(char) {
return char >= '0' && char <= '9';
}
/**
* Given a JavaScript 2d Array, this function returns the transposed table.
* Arguments:
* - data: JavaScript 2d Array
* Returns a JavaScript 2d Array
* Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].
*/
function arrayTranspose_(data) {
if (data.length == 0 || data[0].length == 0) {
return null;
}
var ret = [];
for (var i = 0; i < data[0].length; ++i) {
ret.push([]);
}
for (var i = 0; i < data.length; ++i) {
for (var j = 0; j < data[i].length; ++j) {
ret[j][i] = data[i][j];
}
}
return ret;
}
/**
* Given a RichTextValue Object, iterate over the individual runs
* and call out to htmlStyleRtRun() to return the text wrapped
* in <span> tags with specific styling.
* @param {RichTextValue} richTextValue a RichTextValue object
* from a given Cell.
* @return {string} HTML encoded text
*/
function htmlEncodeRichText_(richTextValue) {
return richTextValue.getRuns().reduce((s, r) => {
text = r.getText().replace(/\n/g, "<br>");
style = r.getTextStyle();
if (style.isStrikethrough()) {
text = "<strike>" + text + "</strike>";
}
if (style.isUnderline()) {
text = "<u>" + text + "</u>";
}
if (style.isBold()) {
text = "<b>" + text + "</b>";
}
if (style.isItalic()) {
text = "<i>" + text + "</i>";
}
if (style.getFontSize() > 11) {
text = "<big>" + text + "</big>";
}
if (style.getFontSize() < 11) {
text = "<small>" + text + "</small>";
}
return s + text;
}, "");
}
@Mike4U
Copy link

Mike4U commented Jul 3, 2024

@florentdescroix Just wanted to say thanks for some nice code. Forced me to remember GoogleDoc stuff and I'm no expert so I guess your tutorial, comments and links are good enough!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment