updated by GasGit automation
This commit is contained in:
parent
de91d3c689
commit
f0f8d3d3fb
1 changed files with 460 additions and 0 deletions
460
scripts/SheetsMore.js
Normal file
460
scripts/SheetsMore.js
Normal file
|
@ -0,0 +1,460 @@
|
||||||
|
|
||||||
|
/*
|
||||||
|
* extend SpreadsheetApp with some stuff from the
|
||||||
|
* the sheets v4 API for this to work
|
||||||
|
* @constructor SheetsMore
|
||||||
|
*/
|
||||||
|
var SheetsMore = function() {
|
||||||
|
|
||||||
|
var self = this, accessToken_, id_, filters_, applyFilterViews_ =true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether to apply filterviews
|
||||||
|
* @param {boolean} applyViews whether to apply
|
||||||
|
* @return {SheetsMore} self
|
||||||
|
*/
|
||||||
|
self.enableFilterViews = function (applyViews) {
|
||||||
|
applyFilterViews_ = applyViews;
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
// synonym to be deprecataed
|
||||||
|
self.setApplyFilterViews = self.enableFilterViews;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set filters in place in this sheet
|
||||||
|
* @return {SheetsMore} self
|
||||||
|
*/
|
||||||
|
self.applyFiltersToData = function() {
|
||||||
|
|
||||||
|
var result = urlExecute_ (id_, [encodeFields_ (getSheetIdDefs_() , getFilterViewDefs_() , getFilterDefs_())]);
|
||||||
|
if (result.success) {
|
||||||
|
filters_ = result.data.sheets;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw JSON.stringify(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
// deprecate this eventually
|
||||||
|
self.applyFilters = self.applyFiltersToData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* see if any filters overlap
|
||||||
|
* @param {Range} range the range=SpreadsheetApp.getActiveRange()
|
||||||
|
* @return {boolean} if filters overlap
|
||||||
|
*/
|
||||||
|
self.isDataFiltered = function (range) {
|
||||||
|
return getFiltered_(range).length ? true : false;
|
||||||
|
};
|
||||||
|
// to be deprecated
|
||||||
|
self.isFiltered = self.isDataFiltered;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set up the access token
|
||||||
|
* you can use script app.gettoken for this
|
||||||
|
* @param {string} accessToken the token to use
|
||||||
|
* @return {SheetsMore} self
|
||||||
|
*/
|
||||||
|
self.setAccessToken = function (accessToken) {
|
||||||
|
accessToken_ = accessToken;
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the spreadsheet
|
||||||
|
* @param {string} id the spreadsheet id
|
||||||
|
* @return {SheetsMore) self
|
||||||
|
*/
|
||||||
|
self.setId = function (id) {
|
||||||
|
id_ = id;
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the spreadsheet
|
||||||
|
* @return {string} the id
|
||||||
|
*/
|
||||||
|
self.getId = function () {
|
||||||
|
return id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the values, but apply the filter if there is one
|
||||||
|
* @param {Range} range the range that the values are required for
|
||||||
|
* @param {[string]} params any parameters
|
||||||
|
* @return {object} the values {values:[[]], filtered:[[]],isFilter:boolean}
|
||||||
|
*/
|
||||||
|
self.getValues = function (range, params) {
|
||||||
|
|
||||||
|
/* this is API version - better to use the built in apps script to get the values
|
||||||
|
var result = urlExecute_ (id_+'/values/'+range.getSheet().getName() + "!" + range.getA1Notation(),params);
|
||||||
|
var values = result.data.values;
|
||||||
|
|
||||||
|
//found somethig different here - the array is jagged in this API, so pad it out to make it look like apps script
|
||||||
|
var maxWidth = result.data.values.reduce(function (p,c) {
|
||||||
|
return Math.max (c.length, p);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
var values = result.data.values.map (function (d) {
|
||||||
|
for (var i = d.length; i < maxWidth ;i++) {
|
||||||
|
d.push("");
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// get values with Apps Script replaces the API version above.
|
||||||
|
var values = Utils.expBackoff( function () {
|
||||||
|
return range.getValues();
|
||||||
|
});
|
||||||
|
|
||||||
|
// get any filtering that needs to be done
|
||||||
|
var filters = getFiltered_(range);
|
||||||
|
|
||||||
|
|
||||||
|
// play around with validation for filter rules .. dont think this approach will be viable
|
||||||
|
var play = null; // probably wont do this... containsConditions_(filters) ? copyToValidate_ (range) : null;
|
||||||
|
|
||||||
|
// filter those values
|
||||||
|
var filterMap = filterMap_ (filters, values,range);
|
||||||
|
|
||||||
|
// deletet that play sheet
|
||||||
|
if (play) {
|
||||||
|
Utils.expBackoff (function () {
|
||||||
|
return play.getParent().deleteSheet(play);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allValues:values,
|
||||||
|
filteredValues:filters.length ? filterMap.map(function(d) { return values[d]; }) : values,
|
||||||
|
filters:filters,
|
||||||
|
filterMap:filterMap
|
||||||
|
};
|
||||||
|
|
||||||
|
// see if there are any conditions int he filters
|
||||||
|
function containsConditions_ (filters) {
|
||||||
|
return filters.some (function (d) {
|
||||||
|
return d.criteria && Object.keys(d.criteria).some(function (e) {
|
||||||
|
return d.criteria[e].condition;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* see what filters overlap
|
||||||
|
* @param {Range} range the range=SpreadsheetApp.getActiveRange()
|
||||||
|
* @return {[object]} array of objects overlapping this range
|
||||||
|
*/
|
||||||
|
function getFiltered_ (range) {
|
||||||
|
|
||||||
|
// the given range
|
||||||
|
var sci = range.getColumn(),
|
||||||
|
sri = range.getRowIndex(),
|
||||||
|
eci = range.getNumColumns()+ sci -1,
|
||||||
|
eri = range.getNumRows() + sri - 1,
|
||||||
|
sheet = range.getSheet(),
|
||||||
|
sheetId = sheet.getSheetId();
|
||||||
|
|
||||||
|
// for this sheets, find any matching-
|
||||||
|
return (filters_||[]).filter (function (d) {
|
||||||
|
return d.properties.sheetId === sheetId;
|
||||||
|
}).reduce(function (p,c) {
|
||||||
|
if (c.basicFilter && overlap_ (c.basicFilter.range)) {
|
||||||
|
p.push (c.basicFilter);
|
||||||
|
}
|
||||||
|
if (applyFilterViews_) {
|
||||||
|
(c.filterViews || []).forEach(function (d) {
|
||||||
|
if ( overlap_ (d.range)) p.push (d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},[]);
|
||||||
|
|
||||||
|
function overlap_ (ob) {
|
||||||
|
// start row/column are zero based, ends are 1 based
|
||||||
|
return !ob || !( sci > ob.endColumnIndex || eci < ob.startColumnIndex +1 || sri > ob.endRowIndex || eri < ob.startRowIndex +1);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Local functions
|
||||||
|
|
||||||
|
function copyToValidate_ (range) {
|
||||||
|
|
||||||
|
// add a sheet to do validation stuff in
|
||||||
|
var sheet= range.getSheet();
|
||||||
|
return Utils.expBackoff ( function () {
|
||||||
|
var copy = sheet.copyTo(sheet.getParent());
|
||||||
|
copy.setName("delete-" + sheet.getName() + "-" + new Date().getTime().toString(32));
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMap_ (filters, values,range) {
|
||||||
|
|
||||||
|
// generate a map to show which row indexes were included
|
||||||
|
// TODO some offset work if the values don't correspond to the sheet datarange...
|
||||||
|
// if the filterrange,valuesrange start at different places there's a bug in here that I havent tracked down
|
||||||
|
// also Romain reported that he's found some filters with no crieria column definiion in some sheets
|
||||||
|
// not sure what that means to the logic, but for now they wont work
|
||||||
|
// if anyone figures out how to treat such filters tehn comment here
|
||||||
|
var rowStartOffset = range.getRow() - 1;
|
||||||
|
var colStartOffset = range.getColumn() - 1;
|
||||||
|
|
||||||
|
// this is the offset within the values
|
||||||
|
var rowOffset = 0;
|
||||||
|
|
||||||
|
return values.reduce(function (p,row) {
|
||||||
|
|
||||||
|
if (filters.every(function (f) {
|
||||||
|
|
||||||
|
return !f.criteria || Object.keys(f.criteria).every(function(c){
|
||||||
|
// the criteria column numbers are 0 based
|
||||||
|
var colOffset = parseInt (c , 10);
|
||||||
|
|
||||||
|
// but the effective column needs to take account of the start of the tested range & filter range
|
||||||
|
var valuesColumn = colOffset + f.range.startColumnIndex - colStartOffset;
|
||||||
|
|
||||||
|
// returns true if it's a keepable value
|
||||||
|
var keep = true;
|
||||||
|
|
||||||
|
// always keep the header row,, otherwise check if its within range
|
||||||
|
|
||||||
|
if ( (rowOffset || rowStartOffset !==f.range.startIndex) && applies_(f.range,rowOffset + rowStartOffset,colOffset + colStartOffset)) {
|
||||||
|
|
||||||
|
// there are no hidden value matches
|
||||||
|
|
||||||
|
if (keep && f.criteria[colOffset].hiddenValues) {
|
||||||
|
keep = !f.criteria[colOffset].hiddenValues.some(function (h) {
|
||||||
|
return matches_ (row[valuesColumn] , h) ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// there are no hidden value matches
|
||||||
|
if (keep && f.criteria[colOffset].condition) {
|
||||||
|
keep = conditionValueMatches_ (row[valuesColumn] , f.criteria[colOffset].condition) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return keep;
|
||||||
|
|
||||||
|
});
|
||||||
|
})){
|
||||||
|
p.push (rowOffset);
|
||||||
|
}
|
||||||
|
rowOffset++;
|
||||||
|
return p;
|
||||||
|
},[]);
|
||||||
|
|
||||||
|
function applies_ (ob,rowOffset, colOffset) {
|
||||||
|
// starts seem to be offsets (0 based), ends are indexes (1 based)
|
||||||
|
return !ob || (
|
||||||
|
!(colOffset >= ob.endColumnIndex || colOffset < ob.startColumnIndex || rowOffset >= ob.endRowIndex || rowOffset < ob.startRowIndex));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// there will probably be fuzzy versions of this to introduce
|
||||||
|
function matches_ (a,b) {
|
||||||
|
// do a == instead of === for now as types could be all mixed up -- need to check how filter handles different types and adjust later.
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there will probably be fuzzy versions of this to introduce .. TODO
|
||||||
|
function conditionValueMatches_ (a,condition) {
|
||||||
|
try {
|
||||||
|
return conditionValueMatch_[condition.type] (a,condition);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
throw new Error('condition ' + condition.type + ' not yet implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//
|
||||||
|
var conditionValueMatch_ = (function () {
|
||||||
|
|
||||||
|
// utility matchers
|
||||||
|
function textContains (a,b) {
|
||||||
|
return a.indexOf(b) !== -1;
|
||||||
|
}
|
||||||
|
function isNumber(a) {
|
||||||
|
return typeof a === 'number';
|
||||||
|
}
|
||||||
|
function checkArgs(condition,n) {
|
||||||
|
if (condition.values.length !== n) throw new Error ('condition ' + condition.type + ' requires ' + n + ' values');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// also a value can be a formula
|
||||||
|
// if its a number test.. then non numbers are filtered out
|
||||||
|
return {
|
||||||
|
TEXT_NOT_CONTAINS: function (a,b) {
|
||||||
|
return checkArgs (b,1) && !textContains (a,b.values[0].userEnteredValue);
|
||||||
|
},
|
||||||
|
TEXT_CONTAINS: function (a,b) {
|
||||||
|
return checkArgs (b,1) && textContains (a,b.values[0].userEnteredValue);
|
||||||
|
},
|
||||||
|
NUMBER_GREATER: function (a,b) {
|
||||||
|
return checkArgs (b,1) && isNumber(a) && a > b.values[0].userEnteredValue;
|
||||||
|
},
|
||||||
|
NUMBER_GREATER_THAN_EQ: function (a,b) {
|
||||||
|
return checkArgs (b,1) && isNumber(a) && b.values[0].userEnteredValue > a;
|
||||||
|
},
|
||||||
|
NUMBER_LESS: function (a,b) {
|
||||||
|
return checkArgs (b,1) && isNumber(a) && a < b.values[0].userEnteredValue;
|
||||||
|
},
|
||||||
|
NUMBER_LESS_THAN_EQ: function (a,b) {
|
||||||
|
return checkArgs (b,1) && isNumber(a) && b.values[0].userEnteredValue < a;
|
||||||
|
},
|
||||||
|
NUMBER_EQ: function (a,b) {
|
||||||
|
return checkArgs (b,1) && isNumber(a) && a === b.values[0].userEnteredValue;
|
||||||
|
},
|
||||||
|
NUMBER_NOT_EQ: function (a,b) {
|
||||||
|
return checkArgs (b,1) && a !== b.values[0].userEnteredValue;
|
||||||
|
},
|
||||||
|
TEXT_STARTS_WITH: function (a,b) {
|
||||||
|
return checkArgs (b,1) && a.match(new regExp ("^" + b.values[0].userEnteredValue,"m"));
|
||||||
|
},
|
||||||
|
TEXT_ENDS_WITH: function (a,b) {
|
||||||
|
return checkArgs (b,1) && a.match(new regExp ( b.values[0].userEnteredValue + "$","m"));
|
||||||
|
},
|
||||||
|
TEXT_EQ: function (a,b) {
|
||||||
|
return checkArgs (b,1) && a === b.values[0].userEnteredValue;
|
||||||
|
},
|
||||||
|
NUMBER_BETWEEN: function (a,b) {
|
||||||
|
return checkArgs (b,2) && isNumber(a) && a >= b.values[0].userEnteredValue && a <= b.values[1].userEnteredValue;
|
||||||
|
},
|
||||||
|
NUMBER_NOT_BETWEEN: function (a,b) {
|
||||||
|
return checkArgs (b,2) && (a < b.values[0].userEnteredValue || a > b.values[1].userEnteredValue || !isNumber(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* examples
|
||||||
|
[{criteria={2={condition={values=[{userEnteredValue=2}, {userEnteredValue=3}], type=NUMBER_BETWEEN}}}, range={endColumnIndex=3, endRowIndex=8, sheetId=1786114223, startColumnIndex=0, startRowIndex=0}}]
|
||||||
|
[{criteria={0={condition={values=[{userEnteredValue=hide}], type=TEXT_NOT_CONTAINS}}}, range={endColumnIndex=2, endRowIndex=5, sheetId=1750522359, startColumnIndex=0, startRowIndex=0}}]
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* execute a API request
|
||||||
|
* @param {string} urlTail the url appendage
|
||||||
|
* @param {[string]} [params] the params
|
||||||
|
* @param {string} [options] any options to be merged in
|
||||||
|
* @return {object} a standard response object
|
||||||
|
*/
|
||||||
|
function urlExecute_ ( urlTail , params , options) {
|
||||||
|
|
||||||
|
// set default options
|
||||||
|
options = Utils.vanMerge ([{
|
||||||
|
method:"GET",
|
||||||
|
muteHttpExceptions:true,
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + accessToken_
|
||||||
|
}
|
||||||
|
}, options]);
|
||||||
|
|
||||||
|
// the param string
|
||||||
|
if (params) {
|
||||||
|
var paramString = params.isArray ? params.join ("&") : params;
|
||||||
|
}
|
||||||
|
paramString = paramString ? "?"+paramString : "";
|
||||||
|
|
||||||
|
var response = Utils.expBackoff( function () {
|
||||||
|
return UrlFetchApp.fetch(getBaseUrl_ () + urlTail + paramString, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// trnsmit what happened
|
||||||
|
if (response.getResponseCode() !== 200) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
response:response,
|
||||||
|
success:false,
|
||||||
|
err:response.getContentText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
var ob = JSON.parse (response.getContentText());
|
||||||
|
|
||||||
|
return{
|
||||||
|
response:response,
|
||||||
|
data:ob,
|
||||||
|
success:!ob.error,
|
||||||
|
err:ob.error
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return {
|
||||||
|
response:response,
|
||||||
|
success:false,
|
||||||
|
err:err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the base API Url
|
||||||
|
* @return {string} the base api url
|
||||||
|
*/
|
||||||
|
function getBaseUrl_ () {
|
||||||
|
return "https://sheets.googleapis.com/v4/spreadsheets/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* these are the partial field definitions for basic filter definitions
|
||||||
|
* @return {string} fields needed
|
||||||
|
*/
|
||||||
|
function getFilterDefs_ () {
|
||||||
|
return 'sheets(basicFilter(criteria,range))';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* these are the partial field definitions for basic filter definitions
|
||||||
|
* @return {string} fields needed
|
||||||
|
*/
|
||||||
|
function getFilterViewDefs_ () {
|
||||||
|
return 'sheets(filterViews(criteria,range))';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the sheetid
|
||||||
|
* @return {string} id field
|
||||||
|
*/
|
||||||
|
function getSheetIdDefs_ () {
|
||||||
|
return 'sheets(properties(sheetId,title))';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* encode partial field definitions
|
||||||
|
* @param {[string]} varArray actually this is any number of args
|
||||||
|
* @return {string} the encoded fields
|
||||||
|
*/
|
||||||
|
function encodeFields_ () {
|
||||||
|
if (arguments.length) {
|
||||||
|
return "fields="+encodeURIComponent(Array.prototype.slice.apply(arguments).join(","));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue