In Google Earth Engine Developer's Guide, there is a recommendation to avoid for() loops. They recommend to use map() function as this example:
// to avoid
var clientList = [];
for(var i = 0; i < 8; i++) {
clientList.push(i + 1);
}
print(clientList);
// to use
var serverList = ee.List.sequence(0, 7);
serverList = serverList.map(function(n) {
return ee.Number(n).add(1);
});
print(serverList);
I'm trying to select MODIS scenes from each month/year prior to compute VCI. So, the approach I'd take is with a double loop:
modis = ee.ImageCollection("MODIS/MYD13A1");
var modis_list = [];
for(var i = 1; i <13; i++) {
for(var j = 2000; j <2018; j++){
modis_list.push(modis.filter(ee.Filter.calendarRange(i, i, 'month'))
.filter(ee.Filter.calendarRange(j, j, 'year')));
}
}
print(modis_list);
Is there a way to replicate a double loop like this with map() function to reach a server-side approach?
The easy way to do this is with a single map over the "months" you care about.
// Collect images for each month, starting from 2000-01-01.
var months = ee.List.sequence(0, 18*12).map(function(n) {
var start = ee.Date('2000-01-01').advance(n, 'month')
var end = start.advance(1, 'month')
return ee.ImageCollection("MODIS/MYD13A1").filterDate(start, end)
})
print(months.get(95))
This will return a list of ImageCollections. Most months will have only 1 image, since MYD13A1 contains 16-day images, but some will have two. Month 95 is Jan of 2008 and has two.
Alternatively, you could join the collection with a collection of dates, but this is simpler.
And you should prefer filterDate over calendarRange when possible, as it's optimized.
Assuming that you are just trying to understand GEE's map() function, and how would be the equivalent of a normal js for loop, the code would be:
var map_m = function(i) {
i = ee.Number(i)
var years = ee.List.sequence(2000, 2017)
var filtered_col = years.map(function(j) {
var filtered = modis.filter(ee.Filter.calendarRange(i, i, 'month'))
.filter(ee.Filter.calendarRange(j, j, 'year'))
return filtered
})
return filtered_col
}
var months = ee.List.sequence(1, 12)
var modis_list2 = months.map(map_m).flatten()
This code replicates a normal for loop. First, it goes item by item of the years list, and then item by item of the months list, and then, once you have year and month, filter the collection and add it to a list (map does that automatically). As you use 2 map functions (one over years and the other over months), you get a list of lists, so to get a list of ImageCollection use the flatten() function. Somehow the printed objects are a bit different, but I am sure the result is the same.
Let me start by saying I know nothing about Google Earth Engine and my info is from functional programming knowledge.
map is unique in that it doesn't generate the things it loops over. You start with a list and map iterates over each item in that list and transforms it. If you don't have that list then map isn't a great fit.
It looks like you are creating a list with each month/year combo represented. I would break this into a few steps. Build the month and year lists, build the list that represents cartesian product of the 2 lists then transform to the ee objects.
var range = (from, to) => new Array(end-start+1).fill(0).map((_,i)=>i+from)
var cartesianProduct = (a, b) => // not gonna do this here but it returns pairs [ [ a[1], b[1] ], ... ]
var getEE = ([month, year]) => modis
.filter(ee.Filter.calendarRange(month, month, 'month'))
.filter(ee.Filter.calendarRange(year, year, 'year'));
var months = range(1,12);
var years = range(2000, 2017);
var data = cartesianProduct(months, years)
.map(getEE)
There are likely better ways(like not iterating throught the whole list of modis each time we want a single month object (use a dictionary)) but the gist is the same.
Related
I am trying to create a multiple choice question form to be created from data in a google spreadsheet. I managed to create the form of 60 questions each with 4 choices and setting the correct choice based on the information I have in the spreadsheet.
Last thing I need to do is to insert the correct feedback for each question based on column G in my spreadsheet that contains the feedback for each question.
Edit: here is a picture of how my spreadsheet & form would look like
Picture for Spreadsheet
Picture for how the form questions should look like
Picture of how the form questions look like (without a feedback)
The problem is that is not being implemented, The maximum I could was to set a fixed feedback/word for all questions, but was not possible to import the specific feedback for each question to the feedback section of each question, could anyone help with that, below is my code:
function popForm() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Sheet1');
var numberRows = sheet.getDataRange().getNumRows();
var myQuestions = sheet.getRange(1,1,numberRows,1).getValues();
var myAnswers = sheet.getRange(1,2,numberRows,1).getValues();
var myGuesses = sheet.getRange(1,2,numberRows,4).getValues();
var myfeedback = sheet.getRange(1,7,numberRows,1).getValues();
var myShuffled = myGuesses.map(shuffleEachRow);
Logger.log(myShuffled);
Logger.log(myAnswers);
// Create the form as a quiz. The resulting form's "Quiz options" are different from a manually created quiz. Be aware (and change manually if needed!
var form = FormApp.create('Fast Track Question - Domain I');
form.setIsQuiz(true);
// Write out each multiple choice question to the form.
for(var i=0;i<numberRows;i++){
if (myShuffled[i][0] == myAnswers[i][0]) {
var addItem = form.addMultipleChoiceItem();
addItem.setTitle(myQuestions[i][0])
.setPoints(1)
.setChoices([
addItem.createChoice(myShuffled[i][0],true),
addItem.createChoice(myShuffled[i][1]),
addItem.createChoice(myShuffled[i][2]),
addItem.createChoice(myShuffled[i][3])
]);
var incorrectFeedback = FormApp.createFeedback()
.setText(myfeedback[i][7])
.build();
addItem.setFeedbackForIncorrect(incorrectFeedback);
}
else if (myShuffled[i][1] == myAnswers[i][0]) {
var addItem = form.addMultipleChoiceItem();
addItem.setTitle(myQuestions[i][0])
.setPoints(1)
.setChoices([
addItem.createChoice(myShuffled[i][0]),
addItem.createChoice(myShuffled[i][1],true),
addItem.createChoice(myShuffled[i][2]),
addItem.createChoice(myShuffled[i][3])
]);
var incorrectFeedback = FormApp.createFeedback()
.setText(myfeedback[i][7])
.build();
addItem.setFeedbackForIncorrect(incorrectFeedback);
}
else if (myShuffled[i][2] == myAnswers[i][0]) {
var addItem = form.addMultipleChoiceItem();
addItem.setTitle(myQuestions[i][0])
.setPoints(1)
.setChoices([
addItem.createChoice(myShuffled[i][0]),
addItem.createChoice(myShuffled[i][1]),
addItem.createChoice(myShuffled[i][2],true),
addItem.createChoice(myShuffled[i][3])
]);
var incorrectFeedback = FormApp.createFeedback()
.setText(myfeedback[i][7])
.build();
addItem.setFeedbackForIncorrect(incorrectFeedback);
}
else if (myShuffled[i][3] == myAnswers[i][0]) {
var addItem = form.addMultipleChoiceItem();
addItem.setTitle(myQuestions[i][0])
.setPoints(1)
.setChoices([
addItem.createChoice(myShuffled[i][0]),
addItem.createChoice(myShuffled[i][1]),
addItem.createChoice(myShuffled[i][2]),
addItem.createChoice(myShuffled[i][3],true)
]);
var incorrectFeedback = FormApp.createFeedback()
.setText(myfeedback[i][7])
.build();
addItem.setFeedbackForIncorrect(incorrectFeedback);
}
}
}
// This function, called by popForm, shuffles the 5 choices.
function shuffleEachRow(array) {
var i, j, temp;
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
Proposed change to script
Your code was long and I found it easier to re-write it with a few extra tools such as getDataRange, push and splice and forEach.
It seemed you were calling the methods in the right way, but since you were having to repeat yourself in a few places and keep track of many arrays and indices, it is likely that a small mistake came up.
This is a working script adapted from yours:
function createQuiz() {
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
// Instead of getting individual ranges, it is more efficient
// to get all the data in one go, and then operate on the two
// dimensional array in memory.
let range = sheet.getDataRange();
let values = range.getValues();
// Here I am using a existing form to test, but you can just
// create a new one if you want.
var form = FormApp.openById("[TESTING_ID]");
form.setIsQuiz(true);
values.shift(); // Using this to remove the first row of headers
// Going through each line using a forEach to create a
// multiple choice question
values.forEach(q => {
let choices = [q[1], q[2], q[3], q[4]];
let title = q[0];
let feedback = q[5]
// Calling function to create multiple choice question
createShuffledChoices(form, title, choices, feedback)
});
}
function createShuffledChoices(form, title, choices, feedback){
let item = form.addMultipleChoiceItem();
item.setTitle(title)
.setPoints(1)
// Setting up the array that will be passed into item.setChoices()
let shuffledChoices = [];
// Making sure that the correct answer is only marked once
let correctAnswerChosen = false;
// I found I had to shuffle the questions within the process of
// creating choices as it made it easier to maintain the spreadsheet
for (let i = choices.length; i != 0; i--) {
let rand = Math.floor(Math.random() * (i - 1));
// If the first answer is chosen, it is the correct one.
if (rand == 0 && correctAnswerChosen == false) {
// Combination of push and splice to remove from ordered array
// to the shuffled one
shuffledChoices.push(item.createChoice(choices.splice(rand, 1)[0], true));
// Marking the correct answer as chosen,
// so that no others are marked correct.
correctAnswerChosen = true;
} else {
shuffledChoices.push(item.createChoice(choices.splice(rand, 1)[0]));
}
}
// Finally setting the choices.
item.setChoices(shuffledChoices);
// Creating the feedback
let formFeedback = FormApp.createFeedback().setText(feedback).build();
item.setFeedbackForIncorrect(formFeedback);
}
The way that you were creating feedback was correct, I suspect that you were just getting mixed up with your arrays and indexes. This is why I tried to simplify your code and eliminate repeated sections.
I combined the shuffling process with the creation of the multiple choice question. This is because the shuffled array that is passed into item.setChoices has to be built of item.createChoice objects. This can't be done in another scope because item is not available.
Combining the logic for shuffling this way means that you don't need to have the letter prefixes in your questions A). You also don't need the column that has the correct answer, because the process knows that the first answer is the correct one. So your sheet can be simplified to this:
For this script to work, the data needs to be organized in this way. (Though you can adapt it anyway you like of course)
References
getDataRange
push
splice
shift
forEach
I am using google sheets quite a lot, but now I am trying to use google apps script to get and update dynamic data retrieved from formulas into a static table.
So, I have a sheet called 'dynamique', with formulas retrieving, filtering and sorting data from other spreadsheets.
I want to be able to work on this data, so I am trying to create a button which would copy all the values from the 'dynamique' sheet into another sheet called 'statique'. That is, I want a formula which would check if the values from the column C of the 'dynamique' sheet are in the column C of the 'statique' sheet. And if the values aren't there, I want the script to copy them. (columns A and B are empty)
I've managed to get my script to work for one column, but now, I want to copy the whole line.
For example, if the value in dynamique!C10 can't be found in statique!C:C, my script writes the value of dynamique!C10 in the first empty cell of the column statique!C:C. But I want it to write dynamique!C10:J10 into my destination sheet (say it's going to be maybe statique!C8:J8).
Here is my code, working for only one cell.
function dynamicToStatic() {
var dynSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("dynamique");
var staSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("statique");
var dynLength = dynSheet.getRange("C1:C").getValues().filter(String).length;//.getLastRow();
var staLength = staSheet.getRange("C1:C").getValues().filter(String).length;
var staRange = staSheet.getRange(6,3,staLength-1);
var staValues = staRange.getValues();
var rangeToCheck = dynSheet.getRange(6,3,dynLength-1,8);
var valuesToCheck = rangeToCheck.getValues();
var numRows = rangeToCheck.getNumRows();
var staNumRows = staRange.getNumRows();
for (i = 0; i<= numRows; i++) {
var row = valuesToCheck[i];
var index = ArrayLib.indexOf(staValues , -1 , row);
if (index == -1) {
//if (staValues.indexOf(row) != -1) {
staSheet.getRange(i+6,3,1,8).setValues(row);
}
}
var timestamp = new Date();
staSheet.getRange(4,3).setValue('List updated on the: '+timestamp);
}
Now I can't manage to retrieve the whole line of the array, so as to be able to copy it using range.setValues(). I always get error messages.
Any help would be more than appreciated...
function gettingFullRows() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const shsr=2;//data startrow
const vA=sh.getRange(shsr,1,sh.getLastRow()-shsr+1,sh.getLastColumn()).getValues();
let html='';
vA.forEach((r,i)=>{
html+=Utilities.formatString('<br />Row:%s is %s',i+shsr,r.join(','));
});
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(html), "Row");
}
So i did some re-writing to your code and made some comments in there. I hope this will make some things clear.
Array's are 0 indexed. So if the value is NOT found in the .indexOf then it would return -1. Also (for speed) i first push all the result to a array and then set the array in one "action" this saves a lot of time. The calls to and from a sheet takes the most time.
For the conversion to a 1d array i used spread operator
See this link for difference in const / var / let
The timestamp string i updated with the use of Template literals
If you have some questions, shoot! (also i did not test this ofcourse)
function dynamicToStatic() {
const dynSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("dynamique");
const staSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("statique");
const dynValues = dynSheet.getRange(1,3,dynSheet.getLastRow(),8).getValues();
//This is a 2d array
const staRaw = staSheet.getRange(6, 3, staSheet.getLastRow()).getValues();
//Convert to 1d array, for the indexoff later on, this way it is easier.
const staValues = [].concat(...staRaw);
//to store the output, as a 2d array, inside the if you see i push it in as array so you have the 2d array for the setValues.
const output = [];
for (let i = 0; i < dynValues.length; i++){
//i = the index of the array (row) inside the array of rows, the 0 would be the values of column C.
if (staValues.indexOf(dynValues[i][0]) >= 0){
output.push([dynValues[i]]);
}
}
//Start by the lastrow + 1, column C(3), ouput is a array of arrays(rows), then get the [0].lengt for the columns inside the row array.
staSheet.getRange(staSheet.getLastRow()+1, 3, output.length, output[0].lenght).setValues(output);
const timestamp = new Date();
staSheet.getRange(4,3).setValue(`List updated on the: ${timestamp}`);
}
I have looking into this issue for a while, but have yet to find a suitable answer (most involve switching to setChoiceValues() rather than addressing the "Cannot convert Array to Choice[]" issue with setChoices([])).
While attempting to generate form sections and questions via Google Script, I ran into the issue of not getting my answer selections to go to specific pages based on the user's answer. This appears to be the difference between setChoiceValues() and setChoices([]), with the latter allowing for page navigation as best as I can tell.
However, when attempting to put my array of new choices into setChoices([]), I get the error message "Cannot convert Array to Choice[]". My code works fine otherwise, but I need to use setChoices([]) (it seems) in order to get the page navigation that I want.
How can I loop values into an array or other container and be able to make them appear as a Choices[] object? How can I make something like this work? It seems like it should be much easier than it is, but I cannot see the solution.
Below is a segment of my code that is causing the issue:
//Form - globally accessible
var f = FormApp.openById(f_id);
//Date Iterator
var curr_date = 0;
//Time Iterator
var curr_time = 0;
//Array of Times
var Tchoices = [];
//Setting Time choices per date
while(curr_date < dates.length)
{
Tchoices = [];
curr_time = 0;
//dates is an array of objects with both d's (single date) and t's
// (array of times for that date)
var d = dates[curr_date].d;
var end_break = f.addPageBreakItem().setTitle("Times for " + d);
var f_time = f.addMultipleChoiceItem().setTitle(d);
while(curr_time < dates[curr_date].t.length)
{
end_break = end_break.setGoToPage(FormApp.PageNavigationType.SUBMIT);
Tchoices.push(f_time.createChoice(dates[curr_date].t[curr_time], end_break).getValue());
curr_time++;
}
f_time.setChoices([Tchoices]);
}
There was some minor issues with the building of your MultipleChoise object:
//Form - globally accessible
var f = FormApp.openById('someID');
//Date Iterator
var curr_date = 0;
//Time Iterator
var curr_time = 0;
//Array of Times
var Tchoices = [];
//Setting Time choices per date
while(curr_date < dates.length)
{
Tchoices = [];
curr_time = 0;
//dates is an array of objects with both d's (single date) and t's
// (array of times for that date)
var d = dates[curr_date].d;
var end_break = f.addPageBreakItem().setTitle("Times for " + d);
var f_time = f.addMultipleChoiceItem();
f.addMultipleChoiceItem().setTitle(d);
while(curr_time < dates[curr_date].t.length) //verify this while not sure what you have inside your dates array
{
end_break = end_break.setGoToPage(FormApp.PageNavigationType.SUBMIT);
Tchoices.push(f_time.createChoice(dates[curr_date].t[curr_time])); //You cannot add a pagebreak inside the elements of a choiseItem array
curr_time++;
}
Logger.log(Tchoices);
f_time.setChoices(Tchoices);
}
Check the values for dates[curr_date].t.length inside the Loop I'm not sure how you constructed the array.
You cannot add a pagebreak inside the elements of a choiseItem array
I have an object,
obj = {};
now I am adding items into this object as,
obj[element] = /*something*/
now if I want to access this object for key = element as,
obj[element];
what would be time complexity of this operation.
And please dont suggest using of Array instead of Object, I know array has constant time look up because I am adding elements at random numbers (using them as index), so if Ii use array, I will have a sparse array and that would be inefficient in terms of memory.
It is minimal enough for you to not worry about it. Objects are a core part of javascript, and micro-optimization is bad.
You're much better off writing understandable code (whether it's with objects or what-not) than writing code that saves you 0.000000000001 seconds.
Just you need to know: Array is an Object too in JavaScript.
Also to check speed of any operation you can do something like:
var obj = {}, element = "test";
obj[element] = 333333;
var time1 = new Date().getTime(); // start timestamp
for (var i=0; i<1000; i++)
{
// here can be any code for measurement
var a = obj[element];
}
var time2 = new Date().getTime(); // end timestamp
var result = (time2 - time1) / 1000; // divide all time to number of iterations
alert(result + " ms");
var klas4 = [];
klas4[2] = [];
klas4[2]["hour"] = 1;
klas4[2]["teacher"] = "JAG";
klas4[2]["group"] = "V4A";
klas4[2]["subject"] = "IN";
klas4[2]["classroom"] = "B111";
klas4[0] = [];
klas4[0]["hour"] = 6;
klas4[0]["teacher"] = "JAG";
klas4[0]["group"] = "V4B";
klas4[0]["subject"] = "IN";
klas4[0]["classroom"] = "B111";
klas4[1] = [];
klas4[1]["hour"] = 4;
klas4[1]["teacher"] = "NAG";
klas4[1]["group"] = "V4A";
klas4[1]["subject"] = "NA";
klas4[1]["classroom"] = "B309";
This multidimensional array needs to be sorted by hour, ascending. The problem is, I don't know how to sort an multidimensional array. The first dimension (0, 1 and 2), needs to be changed, according to the hour, but all other details from dimension 2 (teacher, group etc.) also need to change from index, because otherwise the data is mixed.
You don't know how many indexes there are. In this example, the correct sequence should be: klas4[2][...], klas4[1][...], klas[0][...]
In PHP there's a certain function multisort, but I couldn't find this in jQuery or JavaScript.
klas4.sort( function(a,b){ return a.hour - b.hour } );
should do it.
It helps to think of klas4 not as a multi-array but as 1 array of objects.
Then you sort the objects in that array with a sort function.
The sort function takes 2 objects and you must return which one comes first.
You should read on sort() for Array, google that.
Also, as others have commented; the entries for klas4 are really objects, you should use
klas4[2] = {};
or even better
klas4[2] = { hour:1 , teacher:"JAG" , group:"V4A" , subject: "IN" };
Finally, I assume you are a native Dutch or German speaker, as I am. I would strongly suggest to name all your variables in English, class4, not klas4. It is the right, professional thing to do.