Skip to content

Instantly share code, notes, and snippets.

@jeaimehp
Last active July 12, 2024 21:06
Show Gist options
  • Save jeaimehp/2656730f3cd021b59aa845e6f8d483e9 to your computer and use it in GitHub Desktop.
Save jeaimehp/2656730f3cd021b59aa845e6f8d483e9 to your computer and use it in GitHub Desktop.
javascript that you can run in google sheets to export the JSON properly for ingestion into the HPC-ED catalog
// Modified by: Jeaime Powell
// Revision Date: 7/12/24
/*
Purpose:
Using Input data from Google Sheets with column headers matchin HPC-ED API metadata tags and output formatted
JSON for HPC-ED API ingestion
--> New revision accounts for multiple rows
*/
// Modified from https://gist.githubusercontent.com/pamelafox/1878143/raw/6c23f71231ce1fa09be2d515f317ffe70e4b19aa/exportjson.js
// Ref: https://thenewstack.io/how-to-convert-google-spreadsheet-to-json-formatted-text/
// 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.
var FORMAT_ONELINE = 'One-line';
var FORMAT_MULTILINE = 'Multi-line';
var FORMAT_PRETTY = 'Pretty';
var LANGUAGE_JS = 'JavaScript';
var LANGUAGE_PYTHON = 'Python';
var STRUCTURE_LIST = 'List';
var STRUCTURE_HASH = 'Hash (keyed by "id" column)';
/* Defaults for this particular spreadsheet, change as desired */
var DEFAULT_FORMAT = FORMAT_PRETTY;
var DEFAULT_LANGUAGE = LANGUAGE_JS;
var DEFAULT_STRUCTURE = STRUCTURE_LIST;
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [
//{name: "Export JSON for this sheet", functionName: "exportSheet"},
//{name: "Export JSON for all sheets", functionName: "exportAllSheets"},
{name: "Export JSON for HPC-ED", functionName: "exportSheetHPCED"}
];
ss.addMenu("Export JSON for HPC-ED", menuEntries);
}
function makeLabel(app, text, id) {
var lb = app.createLabel(text);
if (id) lb.setId(id);
return lb;
}
function makeListBox(app, name, items) {
var listBox = app.createListBox().setId(name).setName(name);
listBox.setVisibleItemCount(1);
var cache = CacheService.getPublicCache();
var selectedValue = cache.get(name);
Logger.log(selectedValue);
for (var i = 0; i < items.length; i++) {
listBox.addItem(items[i]);
if (items[1] == selectedValue) {
listBox.setSelectedIndex(i);
}
}
return listBox;
}
function makeButton(app, parent, name, callback) {
var button = app.createButton(name);
app.add(button);
var handler = app.createServerClickHandler(callback).addCallbackElement(parent);;
button.addClickHandler(handler);
return button;
}
function makeTextBox(app, name) {
var textArea = app.createTextArea().setWidth('100%').setHeight('200px').setId(name).setName(name);
return textArea;
}
function exportAllSheets(e) {
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, getExportOptions(e));
var sheetName = sheet.getName();
sheetsData[sheetName] = rowsData;
}
var json = makeJSON_(sheetsData, getExportOptions(e));
displayText_(json);
}
function exportSheet(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rowsData = getRowsData_(sheet, getExportOptions(e));
var json = makeJSON_(rowsData, getExportOptions(e));
displayText_(json);
}
/// Added to specifically match the HPC-ED format
function exportSheetHPCED(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rowsData = getRowsDataHPCED_(sheet, getExportOptions(e));
var json = makeJSONHPCED_(rowsData, getExportOptions(e));
displayText_(json);
}
function getExportOptions(e) {
var options = {};
options.language = e && e.parameter.language || DEFAULT_LANGUAGE;
options.format = e && e.parameter.format || DEFAULT_FORMAT;
options.structure = e && e.parameter.structure || DEFAULT_STRUCTURE;
var cache = CacheService.getPublicCache();
cache.put('language', options.language);
cache.put('format', options.format);
cache.put('structure', options.structure);
Logger.log(options);
return options;
}
function makeJSON_(object, options) {
if (options.format == FORMAT_PRETTY) {
var jsonString = JSON.stringify(object, null, 4);
} else if (options.format == FORMAT_MULTILINE) {
var jsonString = JSON.stringify(object);
jsonString = jsonString.replace(/},/gi, '},\n');
jsonString = prettyJSON.replace(/":\[{"/gi, '":\n[{"');
jsonString = prettyJSON.replace(/}\],/gi, '}],\n');
} else {
var jsonString = JSON.stringify(object);
}
if (options.language == LANGUAGE_PYTHON) {
// add unicode markers
jsonString = jsonString.replace(/"([a-zA-Z]*)":\s+"/gi, '"$1": u"');
}
return jsonString;
}
function makeJSONHPCED_(object, options) {
if (options.format == FORMAT_PRETTY) {
var jsonString = JSON.stringify(object, null, 4);
} else if (options.format == FORMAT_MULTILINE) {
var jsonString = JSON.stringify(object);
jsonString = jsonString.replace(/},/gi, '},\n');
jsonString = prettyJSON.replace(/":\[{"/gi, '":\n[{"');
jsonString = prettyJSON.replace(/}\],/gi, '}],\n');
} else {
var jsonString = JSON.stringify(object);
}
if (options.language == LANGUAGE_PYTHON) {
// add unicode markers
jsonString = jsonString.replace(/"([a-zA-Z]*)":\s+"/gi, '"$1": u"');
}
//HPC-ED Reformat
var jsonHPCED = JSON.parse(jsonString);
var newjsonStringHPCED = {"ingest_type":jsonHPCED[0].ingest_type};
if (jsonHPCED.length == 1) {
newjsonStringHPCED.ingest_data = {"subject": jsonHPCED[0].subject,
"visible_to": jsonHPCED[0].visible_to.split(',').map(function (value) {return value.trimStart();}),
"id": jsonHPCED[0].id,
"content" : {
"Title": jsonHPCED[0].Title,
"URL": jsonHPCED[0].URL,
"Resource_URL_Type": jsonHPCED[0].Resource_URL_Type,
"Cost": jsonHPCED[0].Cost,
"Language": jsonHPCED[0].Language,
"Provider_ID": jsonHPCED[0].Provider_ID,
"Authors": jsonHPCED[0].Authors.split(',').map(function (value) {return value.trimStart();}),
"Learning_Resource_Type": jsonHPCED[0].Learning_Resource_Type,
"Abstract": jsonHPCED[0].Abstract,
"Version_date": jsonHPCED[0].Version_date,
"Keywords": jsonHPCED[0].Keywords.split(',').map(function (value) {return value.trimStart();}),
"License": jsonHPCED[0].License,
"Duration": jsonHPCED[0].Duration,
"Expertise_Level": jsonHPCED[0].Expertise_Level.split(',').map(function (value) {return value.trimStart();}),
"Learning_Outcome": jsonHPCED[0].Learning_Outcome.split(',').map(function (value) {return value.trimStart();}),
"Rating": jsonHPCED[0].Rating,
"Start_Datetime": jsonHPCED[0].Start_Datetime,
"Target_Group": jsonHPCED[0].Target_Group.split(',').map(function (value) {return value.trimStart();})
}
};
} else {
newjsonStringHPCED.ingest_data = {"gmeta": [] };
for (let i = 0; i < jsonHPCED.length; i++){
newjsonStringHPCED.ingest_data.gmeta[i] = {"subject": jsonHPCED[i].subject,
"visible_to": jsonHPCED[i].visible_to.split(',').map(function (value) {return value.trimStart();}),
"id": jsonHPCED[i].id,
"content" : {
"Title": jsonHPCED[i].Title,
"URL": jsonHPCED[i].URL,
"Resource_URL_Type": jsonHPCED[i].Resource_URL_Type,
"Cost": jsonHPCED[i].Cost,
"Language": jsonHPCED[i].Language,
"Provider_ID": jsonHPCED[i].Provider_ID,
"Authors": jsonHPCED[i].Authors.split(',').map(function (value) {return value.trimStart();}),
"Learning_Resource_Type": jsonHPCED[i].Learning_Resource_Type,
"Abstract": jsonHPCED[i].Abstract,
"Version_date": jsonHPCED[i].Version_date,
"Keywords": jsonHPCED[i].Keywords.split(',').map(function (value) {return value.trimStart();}),
"License": jsonHPCED[i].License,
"Duration": jsonHPCED[i].Duration,
"Expertise_Level": jsonHPCED[i].Expertise_Level.split(',').map(function (value) {return value.trimStart();}),
"Learning_Outcome": jsonHPCED[i].Learning_Outcome.split(',').map(function (value) {return value.trimStart();}),
"Rating": jsonHPCED[i].Rating,
"Start_Datetime": jsonHPCED[i].Start_Datetime,
"Target_Group": jsonHPCED[i].Target_Group.split(',').map(function (value) {return value.trimStart();})
}
};
}
}
var jsonString = JSON.stringify(newjsonStringHPCED, 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, options) {
var headersRange = sheet.getRange(1, 1, sheet.getFrozenRows(), sheet.getMaxColumns());
var headers = headersRange.getValues()[0];
var dataRange = sheet.getRange(sheet.getFrozenRows()+1, 1, sheet.getMaxRows(), sheet.getMaxColumns());
var objects = getObjects_(dataRange.getValues(), normalizeHeaders_(headers));
if (options.structure == STRUCTURE_HASH) {
var objectsById = {};
objects.forEach(function(object) {
objectsById[object.id] = object;
});
return objectsById;
} else {
return objects;
}
}
function getRowsDataHPCED_(sheet, options) {
var headersRange = sheet.getRange(1, 1, sheet.getFrozenRows(), sheet.getMaxColumns());
var headers = headersRange.getValues()[0];
var dataRange = sheet.getRange(sheet.getFrozenRows()+1, 1, sheet.getMaxRows(), sheet.getMaxColumns());
var objects = getObjects_(dataRange.getValues(), headers); // removed normalizeheaders
if (options.structure == STRUCTURE_HASH) {
var objectsById = {};
objects.forEach(function(object) {
objectsById[object.id] = object;
});
return objectsById;
} else {
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.getNumRows(), 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, keys) {
var objects = [];
for (var i = 0; i < data.length; ++i) {
var object = {};
var hasData = false;
for (var j = 0; j < data[i].length; ++j) {
var cellData = data[i][j];
if (isCellEmpty_(cellData)) {
continue;
}
object[keys[j]] = cellData;
hasData = true;
}
if (hasData) {
objects.push(object);
}
}
return objects;
}
// 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"
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 (!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;
}
@jeaimehp
Copy link
Author

7-12-24 ---> Added ability to ingest multi-row sheets and adjust format as needed.

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