Here is the code for each of the example mentioned in GAS lists and validation.
The custom functions
You need this in your local workbook. You can find it in the examples sheet. These are the custom functions you can call directly from your sheets. The function documentation is here
// custom functions for use with cBlister class // this module is required in any spreadsheet you want to use these custom functions in. // they should need changing // you also need to include the blister library // // v1.1 sep 2013 // creative commons license // bruce@mcpher.com // ramblings.mcpher.com //---------------------- /** * return the values in a blister and return the value(s) in the same position(s) * @param {*} value we're looking for * @param {string} listName the name of the list to find it in * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * @param {string|number=} optReturnListId can be a column name, or a column number starting/default at 1 * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches * return {*|<array>.*} the found value(s) */ function blisterLookup ( listName, value, optListId, optReturnListId , optSortId , optSortDescending, optMaxMatch ) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} ); // match it var idx = b.matchWorker ( value , { listId : optListId || undefined, maxMatch: optMaxMatch || undefined }); // get the values - its already sorted so no need to do it again return blister.transposeArray(b.indexWorker (idx , { listId : optReturnListId || optListId || undefined, })); } /** * get value(s) at the specified index in the given list * @param {string} listName the name of the list to find it in * @param {number|<array>.number} idx the item number starting at 1, or an array of start positions. * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches * return {*|<array>.*} the found value(s) */ function blisterIndex ( listName, idx , optListId, optSortId, optSortDescending ) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } ); return blister.transposeArray(b.indexWorker (idx, { listId : optListId || undefined })); } /** * lookup the values in a blister and return the position of the match(es) * @param {*} value we're looking for * @param {string} listName the name of the list to find it in * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 1 (default) will return a single value, everything else returns an array, 0 is all matches * return {*|<array>.*} the found value(s) */ function blisterMatch (listName,value, optListId, optSortId, optSortDescending, optMaxMatch ) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} ); return blister.transposeArray(b.matchWorker ( value, { listId : optListId || undefined, maxMatch: optMaxMatch || undefined })); } /** * return the unique values in a list column * @param {string} listName the name of the list to find it in * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 0 is all matches and the default * @param {string=} optFilterId can be a column name, or a column number starting at/default 1 * @param {string=} optFilterValue value the filter has to be.. there can be lots of these * return {<array>.*} the array of unique values */ function blisterUnique ( listName, optListId, optSortId, optSortDescending, optMaxMatch ,optFilterId, optFilterValue ) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined} ); return blister.transposeArray(b.uniqueWorker ( { listId : optListId || undefined, maxMatch: optMaxMatch || undefined, filters: makeFilterArgs (5, Array.slice(arguments)) } )); } /** * return the values in a list column * @param {string} listName the name of the list to find it in * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 0 is all matches and the default * @param {string=} optFilterId can be a column name, or a column number starting at/default 1 * @param {string=} optFilterValue value the filter has to be.. there can be lots of these * return {<array>.*} the array of values */ function blisterList ( listName, optListId, optSortId, optSortDescending, optMaxMatch , optFilterId, optFilterValue ) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } ); return blister.transposeArray(b.listWorker ( { listId : optListId || undefined, maxMatch: optMaxMatch || undefined, filters: makeFilterArgs (5, Array.slice(arguments)) } )); } /** * return the headers in a list column * @param {string} listName the name of the list to find it in * @param {string|number=} optListId can be a column name, or a column number starting at/default 1 * return {<array>.*} the array of values */ function blisterHeaders ( listName, optListId ) { // get a new blister var b = blisterAssister (listName, {} ); return b.getListHeaders ( optListId || undefined ); } /** * return the description of a blister * @param {string} listName the name of the list to find it in * return {string} the description */ function blisterDescription ( listName ) { // get a new blister var b = blisterAssister (listName, {} ); return b.getDescription (); } /** * return the last update date of a blister * @param {string} listName the name of the list to find it in * return {string} the description */ function blisterUpdateDate ( listName ) { // get a new blister var b = blisterAssister (listName, {} ); return b.getUpdateDate (); } /** * return the last update date of a blister * @param {string} optLibrary the name of the library to directory - default, this one * return {<array>.String} the descriptions */ function blisterDirectory (optLibrary) { // get a directory of blisters in this library var packages = blister.getBlisterPackages ( { db: blisterPickScriptDb( {library: optLibrary || undefined} )}); var dir =[]; for (var i=0;i<packages.length;i++){ dir.push ([]); dir[dir.length-1].push(packages[i].package.name); dir[dir.length-1].push(packages[i].package.description); } return dir; } /** * return all the values in a list * @param {string} listName the name of the list to find it in * @param {string|number=} optSortId the list id on which to sort before looking, default is not to sort * @param {boolean=} optSortDescending if a sort is required, then whether to sort it descending - default is ascending * @param {number=} optMaxMatch maximum matches to return, 0 is all matches and the default * @param {string=} optFilterId can be a column name, or a column number starting at/default 1 * @param {string=} optFilterValue value the filter has to be.. there can be lots of these * return {<array>.*} the array(s) of values */ function blisterData ( listName, optSortId, optSortDescending, optMaxMatch , optFilterId, optFilterValue) { // get a new blister var b = blisterAssister (listName, {sortId:optSortId || undefined ,sortDescending:optSortDescending || undefined } ); return blister.transposeArray(b.listWorker ( { maxMatch: optMaxMatch || undefined, listId: null, filters: makeFilterArgs (4, Array.slice(arguments)) } )); } /** * return an exsiting or a new blister * @param {string} listName the name of the list to find it in * @param {options} options any options passed to the function * return {cBlister} the blister to use */ function blisterAssister ( listName, options) { // the default place to look is in the scriptDB associated with this sheet. // change it by either specifying a library name that blister knows about // eg. options.library = "blister" (this is an optional param on all the custom functions) // or by passing some library scriptdb below // if you do not want to pass any scriptdbs, just delete the blisterPickScriptDb argument below, and it will only be able // to operate on the global blister list. options = options || {}; // the library can prefix the listName var lib = listName.split ("."); if (lib.length > 1) { listName = lib[1]; options.library = lib[0]; } return blister.blisterHelper (listName, options , blisterPickScriptDb(options) ); } /** * return the scriptDB to pass to blister * @param {options} options any options passed to the function - here we are interested in options.library * return {null|scriptDBInstance} the scriptdb to use */ function blisterPickScriptDb(options) { // to use the library associated with this spreadsheet - options.library is blank // to use another library, options.library should be the name of a library (which needs to be known by this script) // to use the global blister library, options.library = "blister" if (options.library === 'blister' ) { // use global blister library - no action required - blister will take care of it return null; } else { // here you need to find a way to pass the scriptdb of the given library. // by convention, I create a function in any library that needs to share its scriptdb as showMyScriptDb() try { return eval ( (options.library ? options.library + '.' : '' ) + 'showMyScriptDb')(); } catch (e) { throw ("error " + e + " opening library " + options.library); } } } function makeFilterArgs (filterArgStartsAt, args) { var filterArgs; if (args.length > filterArgStartsAt) { filterArgs = []; for ( var i = filterArgStartsAt; i < args.length ; i += 2) { filterArgs.push( {listId:args[i], value:args[i+1] } ); } } return filterArgs; }
The examples showing how to set up validation and populate lists
You need this in your local workbook. You can find it in the examples sheet. These are the custom functions you can call directly from your sheets. The function documentation is here
function generateValidations () { // this is where you maintain the validations to apply to this sheet. return ( { blisters: [ { listName : 'blister.currencies' , blister: [ {listId: 'ISO', sortId: 'ISO' } ], applies: [ {sheet:'enterCurrencies', range:'a2:a'} ] }, { listName : 'car_list' , applies: [ {sheet:'enterCars', range:'a2:c'} ] }, { listName : 'blister.airlines' , applies: [ {sheet:'enterAirlines', range:'a2:a'} ] } ] }) ; } // these need to be in your script but dont need touched function onOpen() { // this runs on opening and applies all the known validations // this applies current known validations for this sheet and should be triggered onOpen return blister.applyValidations( generateValidations(), blisterAssister ) ; } function onEdit(e) { if(e.range.getDataValidations()) { reApplyIfNeeded (e,generateValidations()); } } function reApplyIfNeeded(e, blisterValidations) { return blister.reApply ( e, blisterValidations, blisterAssister); } function blisterFromList () { new blister.cBlister('car_list', { db: showMyScriptDb() , sortId:'make', description: 'local test using car inventory list'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'Sheet1', range:'a:d'}), true); new blister.cBlister('languageCodes', { db: blister.showMyScriptDb() , sortId:'language', description : 'list of language codes by country'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'languageCodes', range:'a:b'}), true); } function loadCurrencies () { new blister.cBlister('currencies', { db: blister.showMyScriptDb() , sortId:'Country', description: 'list of currencies by country'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'Currencies', range:'a:c'}), true); } function loadLanguageCodes () { new blister.cBlister('languages', { db: blister.showMyScriptDb() , sortId:'language', description: 'list of language codes'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'languageCodes', range:'a:b'}), true); } function loadCse() { loadCseLanguageCodes(); loadCseParameters(); } function loadCseLanguageCodes () { new blister.cBlister('cselanguages', { db: blister.showMyScriptDb() , sortId:'language', description: 'list of cse language codes'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'cseLanguages', range:'a:h'}), true); } function loadCseParameters () { new blister.cBlister('cseparameters', { db: blister.showMyScriptDb() , sortId:'item', description: 'list of cse defaults and parameters'}) .makeBlisterFromRange( blister.getRangeFromItem({sheet:'cseParameters', range:'a:b'}), true); } function caution() { deleteAll(blister.showMyScriptDb()); //deleteAll(showMyScriptDb()); //deleteAll(university.showMyScriptDb()); } function deleteAll(db) { while (true) { var result = db.query({}); // get everything, up to limit if (result.getSize() == 0) { break; } while (result.hasNext()) { db.remove(result.next()); } } } function showAll(optDb) { var db = optDb || showMyScriptDb(); var results = db.query({}); while (results.hasNext()) { var result = results.next(); Logger.log(JSON.stringify(result)); } } function showAllBlister() { showAll (blister.showMyScriptDb()); } function showAllUniversity() { showAll (university.showMyScriptDb()); }
The rest of the code is shared out from the blister library . You’ll need to add resource MTP3ATeczKthcdvb_u278yiz3TLx7pV4j to any sheets or scripts using these functions. Documentation is here.
The cBlister Object
/** updated this to be a data abstraction handler */ /** * create a cBlister object * @param {string} name name to give this new blister * @param {object=} optOptions options (db,sortId,sortDescending) * return {cBlister} a new cBlister object */ var cBlister = function (name, optOptions) { var self = this; var pName = name; var pOptions = optOptions || {}; var FAKELISTCOLUMN = '__listColumn'; var pContent = null; var pSilo = new siloItem("", pOptions.db || showMyScriptDb()); var pSortId = pOptions.sortId; var pSortDescending = pOptions.sortDescending; var pDescription = pOptions.description ; if (!pName) throw ('new blister needs a name'); /** * get the name of the blister * return {string} the blister name */ self.name = function () { return pName; } /** * search template for this blister * return {object} the template for querying */ self.template = function () { return { package: { name: self.name() } }; } /** * create an empty data place * return {object} an empty blister */ self.empty = function () { pContent = self.emptyPackage(); return pContent; } /** * create an empty data place * return {object} an empty blister */ self.emptyPackage = function () { var p = self.template(); p.package.items = []; p.package.keys = []; return p; } /** * get the description of the blister * return {string} the blister description */ self.getDescription = function () { var c= self.content(); return (c && c.package.description) ? c.package.description : self.name() ; } /** * get the update date of the blister * return {string} the blister description */ self.getUpdateDate = function () { var c= self.content(); return c ? new Date(c.updateDate) : null ; } /** * get the any options for the blister * return {string} the blister name */ self.options = function () { return pOptions; } self.items = function () { return self.content().package.items } /** * replace or add the blister with current pContent * return {cBlister} the blister for chaining */ self.replace= function (optNewContent) { // write update if (!isUndefined(optNewContent)) pContent = optNewContent; if (!pContent || ! pContent.package) throw ('trying to write null content for ' + self.name()); self.sort(); // if exists, delete it, then write it pSilo.remove (self.template()); pContent.updateDate = new Date().getTime(); pContent.package.description = pDescription || self.getDescription(); pSilo.save (pContent); return self; } /** * sort the current payload according to the column specified in pSortId * return {cBlister} the blister for chaining */ self.sort = function () { // sort the content if (!pSortId || !pContent.package.keys.length || !pContent.package.items.length) return self; var kInx = self.getKeyItemIndex (pSortId) if (kInx === -1) { //.. just dont sortthrow ("invalid sortKey " + pSortId); return self; } // since we have to sort multiple columns, better to sort a map of the data var map = pContent.package.items[kInx].map(function(e, i) { return {index: i, value: e}; }); // sort the map map.sort(function(a, b) { return a.value === b.value ? 0 : ( pSortDescending ? (a.value < b.value ? 1 : -1) : (a.value > b.value ? 1 : -1)) ; }); // reconstitute from the map order for ( var i = 0 ; i < pContent.package.items.length; i++) { var ns = map.map(function(e) { return pContent.package.items[i][e.index]; }); pContent.package.items[i] = ns; } return self; } /** * lookup the values in a blister and return the position of the match(es) * @param {*} value we're looking for * @param {options} options sortId,sortDescending,maxMatch,library * return {*|<array>.*} the found value(s) */ self.matchWorker = function ( value, options ) { options = options || {}; var maxMatch = fixOptional ( options.maxMatch , 1); var data = self.getListValues (options.listId); if (data) { if (maxMatch === 1) { var idx = data.indexOf(value); if(idx != -1) { // all is good return idx+1; } else { throw ("no matches for " + value + " in list " + self.name()); } } else { // will provide an array of results var results = []; for ( var i=0; i < data.length && (results.length < maxMatch || maxMatch === 0 ) ; i++) { if (data[i] === value ) results.push(i+1); } return results; } } else if (isUndefined(data)) { throw (self.name() + ":" + options.listId + ":" + " combination not found in library " + self.options().library); } return data; } /** * get value(s) at the specified index in the given list * @param {number|<array>.number} idx the item number starting at 1, or an array of start positions. * @param {options} options sortId,sortDescending,maxMatch,library * return {*|<array>.*} the found value(s) */ self.indexWorker = function ( idx , options ) { options = options || {}; var listId = options.listId || 1; // get the list required var data = self.getListValues(listId); if (data) { if (isArray(idx)) { var results = []; for (var i =0; i < idx.length ; i++ ){ results.push(data[idx[i]-1]); } return results; } else { return data[idx-1]; } } else if (isUndefined(data)) { throw (self.name() + ":" + listId + ":" + " combination not found in library " + self.options().library); } return data; } /** * add a new column to the list */ self.findOrAddListId = function (listId) { var heading, c = self.content(); // if it's undefined then we'll be adding one with no header if (isUndefined(listId)) { listId = pContent.package.keys.length(); heading = FAKELISTCOLUMN + listId; } else { heading = listId; } // cheack it doesnt exist var k = self.getKeyItemIndex(listId); if (k < 0 ) { // need to add it pContent.package.keys.push(heading); pContent.package.items.push([]); k = pContent.package.keys.length-1; self.replace(); } return k; } /** * given a listId return the column index number * @param {string|number} listId can be a column name, or a column number starting at 1 * return {number} the column index number starting at 0, -1 if doesnt exist */ self.getKeyItemIndex = function(listId) { if(self.content()) { var kName = listId, kInx = parseInt(listId); if ( !isNaN(kInx) && pContent.package.keys.length >= parseInt(listId) && parseInt(listId) >=1 ) { return kInx - 1; } else { for (var i =0 ; i < pContent.package.keys.length ; i++) { if (pContent.package.keys[i] === kName) return i; } return -1; } } else { return -1; } } /** * return the values in a list column * @param {options} options listId,sortId,sortDescending,maxMatch,library * return {<array>.*} the array of values */ self.listWorker = function (options) { options = options || {}; var listId = fixOptional( options.listId , 1); var maxMatch = fixOptional ( options.maxMatch , 0); // get the list required var result = self.getListValues(listId, options.filters); if (result) { if (maxMatch === 0 || result.length === 0) { return result; } else { if (isArray(result[0])) { // its a multi dimentsional thing var data =[]; for ( var i=0; i < result.length ; i++ ) { data.push( result.slice (0,Math.min(maxMatch,result[i].length))); } return data; } else { return [result.slice (0,Math.min(maxMatch,result.length))]; } } } else if (isUndefined(result)) { throw (self.name() + ":" + listId + ":" + " combination not found in library "+JSON.stringify(self.content())); } return result; } /** * given a listId return the headings for that column (or all columns), or null if invalid listId * @param {string|number=} optListId can be a column name, or a column number starting at 1 * return {<Array>.*|null|undefined} the data associated with the given listId */ self.getListHeaders = function(optListId) { var p = self.content(); if (p) { var data = p.package.keys; // narrow down to a particular column if necessary if (optListId) { var idx = self.getKeyItemIndex (optListId); if (idx < 0 ) { return undefined; } else { return data[idx]; } } else { return data; } } return p; } /** * given a listId return the data for that column (or all columns), or null if invalid listId * @param {string|number=} optListId can be a column name, or a column number starting at 1 * @param {<Array>.object|null} optFilters listid and value pairs that have to match for data to be included * return {<Array>.*|null|undefined} the data associated with the given listId */ self.getListValues = function(optListId,optFilters) { var p = self.content(); if (p) { var data = p.package.items; if (optFilters) { // need to do this row-wise, currently its columnwise var data = transposeArray(data); // apply each filter, only including rows that all the filters are true for ( var i=0; i < optFilters.length;i++) { var filter = optFilters[i]; // get the filter column var k = self.getKeyItemIndex (filter.listId); if (k < 0) { return undefined ; } // an undefined filter value will be the same as no filter if (!isUndefined(filter.value)) { data = data.filter ( function (v,ind,tab) { return filter.value === v[k] ; } ); } } // organize back to column wise data = transposeArray(data); } // narrow down to a particular column if necessary if (optListId) { var idx = self.getKeyItemIndex (optListId); if (idx < 0 ) { return undefined; } else { return data[idx]; } } else { return data; } } return p; } /** * return the unique values in a list column * @param {options} options listId,sortId,sortDescending,maxMatch,library * return {<array>.*} the array of unique values */ self.uniqueWorker = function (options) { var listId = options.listId || 1; var maxMatch = fixOptional ( options.maxMatch , 0); // get the list required var data = self.getUniqueListValues(listId, options.sortId, options.filters); if (data) { if (maxMatch === 0) { return data; } else { return data.slice (0,Math.min(maxMatch,data.length)); } } else if (isUndefined(data)) { throw (self.name() + ":" + listId + ":" + " combination not found in library" + self.options().library); } return data; } /** * given a listId return the unique values in a a list * @param {string|number} listId can be a column name, or a column number starting at 1 * @param {boolean} optSort whether to sort the list * return {<Array>.*|null} the unique data values in a list */ self.getUniqueListValues = function (listId, optSort, optFilters) { var item = self.getListValues(listId,optFilters); if (item) { var uniqueList = item.filter ( function ( v , i , t) { return t.indexOf (v) === i; }); if(optSort) uniqueList.sort(); return uniqueList; } return item; } /** * get the existing pContent for a list. First time it will get from scriptDB * @param {boolean} optRefresh if true will always refresh pContent first * return {object} the data as written to scriptDb */ self.content = function (optRefresh) { if (optRefresh) { pContent = self.dbContent(); if(!pContent) { pContent= self.emptyPackage(); } else { self.sort(); } } return pContent; } /** * get the existing pContent for a list. First time it will get from scriptDB * return {object} the data as written to scriptDb */ self.dbContent = function () { // get from scriptdb var results = pSilo.query ( self.template() ); return results.hasNext() ? results.next() : null; } /** * given a range will replace the content for this blister and write it to scriptDB * @param {Range} sourceRange if true will always refresh pContent first * @param {boolean} optHeadings if true will use the first row headings as the the listIDs * @param {boolean} optIncludeEmpty if false will stop at first completely empty row * return {cBlister} the generated blister */ self.makeBlisterFromRange = function(sourceRange,optHeadings,optIncludeEmpty) { pContent = self.getBlisterFromRange (sourceRange, optHeadings,optIncludeEmpty); return self.replace (); } /** * given a range will construct a cBlister payload * @param {Range} sourceRange if true will always refresh pContent first * @param {boolean} optHeadings if true will use the first row headings as the the listIDs * @param {boolean} optIncludeEmpty if false will stop at first completely empty row * return {cBlister} the cBlister payload */ self.getBlisterFromRange = function (r, optHeadings,optIncludeEmpty) { var data = r.getValues(), headings = fixOptional(optHeadings, false), maxRow = 0; var obData = self.emptyPackage(); // srt out headings - can be named from column headings, or numeric if (data.length) { // keys for (var i=0; i < data[0].length; i++) { obData.package.keys.push( headings ? data[0][i] : FAKELISTCOLUMN + (i+1) ); } // data for (var i=0; i < data[0].length; i++) { var d = []; for (var j = (headings ? 1 : 0) ; j < data.length; j++) { d.push( data[j][i] ); if (data[j][i]) maxRow = j; } obData.package.items.push(d); } } // trim trailing blank rows if (!optIncludeEmpty && maxRow < data.length -1) { for (var i=0;i<obData.package.keys.length;i++) { obData.package.items[i] = obData.package.items[i].splice(0,maxRow); } } return obData; } pContent = self.content(true); return self; } /* helps to set up a new blister object */ function blisterHelper (listName,options , optLocalLibrary ) { // if a library is specified then we use that if ( options.library != "blister") { options.db = optLocalLibrary; if (!options.db) throw ("unknown library " + options.library); } return new cBlister (listName, options ); }
cBlister support functions
/**delete everything**/ function deleteAll() { var p = publicStuffDb() ; var results = p.query({}); while (results.hasNext()) { p.remove(results.next()); } } /** * throw an exception if not true * @param {*} arg given value * @param {*} defaultValue value to use if given value IsMissing * @return {*} the new value */ function fixOptional (arg, defaultValue) { if (isUndefined(arg) ){ return defaultValue; } else return arg; } // got this here // https://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ /** * get detailed type of javaScript var * @param {*} obj given item * @return {string} type */ function toType(obj) { return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase() } function isObject (x) { return typeof x === 'object'; } /** * Check if a value is an array * @param {*} arg given item * @return {boolean} true if array */ function isArray (arg) { return toType(arg) === 'array'; } /** * Check if a value is defined * @param {*} arg given value * @return {boolean} true if undefined */ function isUndefined ( arg) { return typeof arg === 'undefined'; } function transposeArray (a) { if (!a) return a; if (!isArray(a) ) return a; if(!a.length) return [[]]; if (!isArray(a[0])) a = [a]; return Object.keys(a[0]).map(function (c) { return a.map(function (r) { return r; }); }); } /** * return the db silo item associated with the given key * @param {string} k the key * @param {ScriptDbInstance=} db the scriptDB * @return {siloItem} the cScriptDbSiloItem */ function siloItem(k,db) { // find the Silo associated with this key var pdb = db; var self = this; var pSiloId =k; /** * remove the silo item with the given key * @param {object} optOb the item to remove, default all of them * @return {siloItem} the siloItem */ self.remove = function (optOb) { var qob = self.template(optOb); // delete all matches var result = pdb.query(qob); while (result.hasNext()) { pdb.remove(result.next()); } return self; } /** * create template for this silo * @param {object|null=} optOb the item to add to the template, default none * @return {object} the cScriptDbSiloItem */ self.template = function (optOb) { var qob = fixOptional(optOb,{}); if(pSiloId)qob.siloId = pSiloId; return qob; } /** * do a query within the silo for this siloItem * @param {object|null=} optOb the query by example (default all data) * @return {ScriptDBquery} a query by example to silo the data */ self.query = function (optOb) { return pdb.query(self.template(optOb)); } /** * do a save within the silo for this siloItem * @param {object} ob the bject to save * @return {object} the object that was saved */ self.save = function (ob) { return pdb.save(self.template(ob)); } return self; } function getBlisterPackages (options) { // get all blisters in a given db var o = options || {}; var s = new siloItem("", o.db || getDefaultDB("blister")); var r = s.query(), t = []; while (r.hasNext()) { t.push (r.next()); } return t; } function getRangeFromItem (item) { return SpreadsheetApp.getActiveSpreadsheet().getSheetByName(item.sheet).getRange(item.range); }
The cValidation object
// use blisters to manage validation // a blisterrule looks like this // { blister: [ {listId:'xx', dependent: true , sortId:'optxx', sortDescending: false},{},{} ..] } function cValidation(b, optBlisterRule) { var self = this; /** * check that these rules go with this blister * @return {boolean} good or not. */ self.validate = function () { for (var i= 0 ; i < pRules.blister.length ; i++ ) { if( pBlister.getKeyItemIndex(pRules.blister[i].listId) < 0 ) return false; } return true; } /** * check that this range goes with this validation * @return {boolean} good or not. */ self.worksWith = function (range) { return (pRules.blister.length === range.getNumColumns() || !pRules.blister.length) && self.validate(); } /** * make an empty blisterRule * @return {boolean} good or not. */ self.emptyBlisterRule = function () { return { blister: [] }; } /** * create a default rule to match this range * @param {Range} range the range to apply it to * @return {object} a default blisterrule */ self.defaultBlisterRule = function (range) { var p = self.emptyBlisterRule(), nc = range.getNumColumns() ; for ( var i = 0 ; i < nc ; i++ ) { var d = { listId: i +1 , sortId: i +1 , filters: null , sortDescending: false}; // filter on previous columns if (i > 0 ) { for ( var filters=[],j = 0; j < i ; j++) { filters.push ( {listId: j+1, value: null } ); } d.filters = filters; } p.blister.push (d); } return p; } /** * apply this validation * @param {Range} range the range to apply it to * @return {cValidation} the self for chaining */ self.apply = function (range) { if (!self.worksWith(range)) throw ("attempt to apply mismatched blister to range"); pRange = range; pValidations = []; // use given rules or set up defaults var r = pRules.blister.length ? pRules.blister : self.defaultBlisterRule(range).blister; var data = null,nr = pRange.getNumRows(), nc= pRange.getNumColumns() ; for ( var i = 0 ; i < r.length ; i ++ ) { if (!r[i].filters) { // we can do the whole column at once var f = pBlister.getUniqueListValues (r[i].listId, r[i].sortId); if (f) { // bugette patch f.push(''); var p = SpreadsheetApp.newDataValidation() .setAllowInvalid(false) .requireValueInList(f ) .build(); pRange.offset(0,i,nr,1).setDataValidation (p); } } else { // we have to do it row by row if (!data) data = pRange.getValues(); for ( var j = 0 ; j < nr ; j++) { // filter on the data values for (var k=0; k < r[i].filters.length; k++) { r[i].filters[k].value = data[j][k]; } // apply dymanic filter to individual cell var f = pBlister.getUniqueListValues (r[i].listId, r[i].sortId, r[i].filters); if (f) { // bugette patch f.push(''); var p = SpreadsheetApp.newDataValidation() .setAllowInvalid(false) .requireValueInList(f) .build(); pRange.offset(j,i,1,1).setDataValidation (p); } } } } return self; } /** * return validations created by this * @return {<array>.DataValidation} the array of validations */ self.getValidations = function () { return pValidations; } /** * return latest used range for this validation * @return {Range} the range */ self.getRange = function () { return pRange; } /** * return latest used range for this validation * @return {cValidation} itself for chaining */ self.setRange = function (range) { pRange = range; return self; } var pBlister = b; if (optBlisterRule && optBlisterRule.blister) { pRules = optBlisterRule; } else { pRules = self.emptyBlisterRule(); } var pRange = null; var pValidations = []; return self; } function intersect (e, target) { // does the range returned by onedit intersect with the one given by onEdit return target.getSheet().getName() == e.getSheet().getName() && target.getRow() <= e.range.rowEnd && target.getLastRow() >= e.range.rowStart && target.getColumn() <= e.range.columnEnd && target.getLastColumn() >= e.range.columnStart ; } function clearKnownValidations(v) { for (var i=0; i < v.blisters.length; i++) { for ( var j =0; j < v.blisters[i].applies.length; j++) { getRangeFromItem(v.blisters[i].applies[j]).clearDataValidations(); } } } function applyValidations (blisterValidations,assister) { if (blisterValidations) { clearKnownValidations (blisterValidations); for (var i=0; i < blisterValidations.blisters.length ; i++ ) { var b = assister ( blisterValidations.blisters[i].listName ) ; var v = new cValidation (b, blisterValidations.blisters[i] ); for ( var j = 0 ; j < blisterValidations.blisters[i].applies.length ; j++ ) { var r = getRangeFromItem(blisterValidations.blisters[i].applies[j]); r.clearDataValidations(); v.apply(r); } } } return blisterValidations; } function reApply(e, blisterValidations,assister) { if (blisterValidations && e.range.getDataValidations()) { // lets see if this intersects with any known validation for (var i=0; i < blisterValidations.blisters.length ; i++ ) { var b = null,v; for ( var j = 0 ; j < blisterValidations.blisters[i].applies.length ; j++ ) { var r = getRangeFromItem(blisterValidations.blisters[i].applies[j]); if ( intersect ( e, r) ) { if (!b) { b = assister ( blisterValidations.blisters[i].listName ) ; v = new cValidation (b, blisterValidations.blisters[i] ); } var eRow = e.range.offset(0,r.offset(0,0,1,1).getColumn()-e.range.getColumn(),1,r.getNumColumns()); eRow.clearDataValidations(); v.apply(eRow); } } } } }
All comments, suggestions, assistance, good lists are welcome as I develop this capability. You can get me on Google plus, Twitter or this forum.