Google Apps Script для создания нормализованных данных из ненормализованных данных в Google Sheets - PullRequest
0 голосов
/ 21 сентября 2018

В Google Sheet у меня есть 3 таблицы с необработанными данными в одной электронной таблице Google ("Category1"), ("Category2"), ("Category3").Эти рабочие листы постоянно обновляются людьми из моего бизнеса, но, к сожалению, данные не в нормированной форме, чтобы можно было выполнять эффективные запросы.

Я хотел бы создать скрипт, который автоматически генерирует нормализованный вывод ('Вывод категории 1 '), (' вывод категории 2 '), (' вывод категории 3 ') этой необработанной информации, которая автоматически обновляется, когда кто-либо вносит изменения в необработанные вкладки.

В приведенном ниже листе Google я привел пример того, как должна выглядеть одна категория.Рабочий лист «Category1» - это сырье, которое постоянно обновляется всеми.«Category1Output» - это конечный рабочий лист, который автоматически обновляется при внесении изменений в рабочий лист «Category1».

Google Sheet Link

1 Ответ

0 голосов
/ 26 октября 2018

Спрашивающий имеет три листа в определенном столбцовом макете - по сути, несколько строк на набор данных и несколько столбцов (по одному на каждый период времени).Эти листы не заменяются, но была запрошена краткая версия, в которой можно было бы использовать фильтрацию, чтобы эффективно сосредоточиться на соответствующих данных.Таким образом, каждый лист должен быть переведен из столбчатого формата в построчный формат.

Сам процесс прост.Исходные данные включали 64 продукта при 8 строках данных на продукт.Выходные записи были @ 1350.

Код опрашивающего зависал при преобразовании данных в выходной формат.Важно использовать 8 строк данных для каждого продукта, и код включает проверку того, что частное от общего числа строк данных, деленное на восемь, является целым числом.Кроме того, имена листа источника и листа вывода называются по имени (getSheetByName), так что код может быть легко применен к любому именованному входному листу и любому названному выходному листу.Единственным условием является то, что оба листа должны существовать заранее.

Первоначальное разрешение проблемы с кодом спрашивающего было успешным, и использование методологии getDataRange и getValues до цикла значительно улучшило производительность,Есть две петли;один с вертикальной ориентацией, перемещающийся по строкам данных;и второй с горизонтальной ориентацией, перемещающийся через связанные со временем столбцы.Однако изначально производительность была очень неэффективной, и код заканчивался по тайм-ауту до завершения.

Я изменил код, чтобы построить один двумерный массив и сохранить его на выходном листе только один раз в конце цикла.Это оказало драматическое влияние на производительность.Общее время выполнения сократилось с нескольких минут до менее чем 5 секунд.


function so5243560403() {

    // Build a clean Output Sheet
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var SourceSheet = ss.getSheetByName("Category2");
    var Target = ss.getSheetByName("Category3Output");

    // Various variables
    var SourceHeaderRow = 9;
    var RowsPerProduct = 8;
    var ProductLengthTruncate = 11;
    var SourceArray = [];
    var i = 0;
    var w = 0;

    // get the bottom of the column
    var ColAvals = SourceSheet.getRange("A" + (SourceHeaderRow + 1) + ":A").getValues();
    var ColAlast = ColAvals.filter(String).length;
    //Logger.log("Last row in column A with data"+ColAlast);  //DEBUG
    var NumberofProducts = ColAlast / RowsPerProduct;
    var lastRow = SourceSheet.getLastRow();

    // Count the products and confirm eight rows each
    var prodtest = isInt1(NumberofProducts);
    if (!prodtest) {
        // Logger.log("NOT an integer!");
        SpreadsheetApp.getUi().alert("Number of Rows divided by rows by Product isn't an integer");
        return false;
    }

    // Get data to clear Target ready for new data
    var TargetlastRow = Target.getLastRow();
    var TargetlastColumn = Target.getLastColumn();
    // clear the content before re-building
    Target.getRange(2, 1, TargetlastRow, TargetlastColumn).clear({
        contentsOnly: true
    });

    // Get ALL the data on the SourceSheet
    var SourceRange = SourceSheet.getDataRange();
    var SourceValues = SourceRange.getValues();

    // create loop for rows of data; first row of data in array=9
    for (i = SourceHeaderRow; i < (SourceHeaderRow + ColAlast); i = i + 8) {

        // create loop for weeks (Week 1=Col5, Week 2=Col6... Week 52=Col56, etc) (actual column numbers are +1)
        for (w = 1; w < 53; w++) {

            // Test to see whether there's a value for Display; the only field ALWAYS populated
            if (SourceValues[i + 1][w + 4]) {

                // Get Product and data fields
                var Prodlen = SourceValues[i][3].length;
                var prodedit = SourceValues[i][3].substring(11, (SourceValues[i][3].length));
                var product = prodedit.trim();
                var catalogue = SourceValues[i][w + 4];
                var display = SourceValues[i + 1][w + 4];
                var ESP = SourceValues[i + 3][w + 4];
                var mechanic = SourceValues[i + 6][w + 4];
                var join1 = product+" | "+display+" | "+mechanic;
                var join2 = display+" | "+product+" | "+mechanic;
                // Start building an array
                SourceArray.push([w, product, catalogue, display, ESP, mechanic,join1,join2]);

            } // end if data exists - process this week

        } // end w - this week loop

    } // end i - this row loop 


    // Copy the data from the array to the Target sheet

    // count number of rows
    var SourceArraylen = SourceArray.length;

    // first row is #2, allowing for header row
    // first column = A
    // number of rows = length of array
    // number of columns = 6 (the fields puched to the array
    var TargetRange = Target.getRange(2, 1, SourceArraylen, 8);

    // set the array values on the Target sheet
    TargetRange.setValues(SourceArray);
}

function isInt1(value) {
    return !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10));
}

ОБНОВЛЕНИЕ

Второй элемент кода спрашивающего связан с обновлением данных до "Выходной лист »по мере внесения изменений в листы« Категория ».Код задающего вопросы для обновления был в порядке, но отсутствовал перевод исходного диапазона на листе категорий для установления целевого диапазона на выходном листе.

Решение включает в себя рубрику, основанную на математической последовательности чисел.В этом случае математическая последовательность представляет собой номера строк для продуктов на исходном листе;каждый продукт занимает 8 строк, а первая строка # 10, поэтому последовательность 10,18,26,34 ....

onEdit возвращает диапазон измененной ячейки и getRow и getColumn может использоваться для определения координат измененной ячейки.Задача состоит в том, чтобы, зная фактический номер строки, который был изменен, установить, какой номер в последовательности строк (и, следовательно, название продукта) представляет фактический номер строки.Также крайне маловероятно (восемь к одному), что измененная строка будет совпадать с первой строкой для номера продукта.

Поэтому для математических последовательностей необходимо применять алгоритм - дважды.Формула для определения n-го числа в последовательности чисел имеет вид An = A1 + (D x (n-1)), где A1 - число для первой строки (в нашем случае 10), D = разница между каждым числомв последовательности (в нашем случае 8), а n = номер в последовательности (в нашем случае измененный номер строки).

Первый проход - установить номер позиции в последовательности чисел(товарные группы) представлены фактически измененной строкой.Весьма вероятно, что результат не является целым числом, т. Е. Он не совпадает с первым рядом для группы продуктов.Таким образом, результат округляется до ближайшего целого числа, и алгоритм обрабатывается второй раз.

Однако на этот раз мы знаем позицию порядкового номера и решаем найти значение числа.В этом случае формула имеет вид ((An-A1) / D) +1.Это вернет номер строки на листе источника, соответствующий первой строке для соответствующей группы продуктов.Мы используем это, чтобы определить, какой тип поля был изменен (Категория, Отображение и т. Д.).

Номер столбца указывает номер недели.Неделя 1 начинается в столбце F, поэтому get column позволяет нам определить, произошло ли изменение в столбце недели (или оно было слева от столбца F).Если слева, то «не моя проблема», если в F или выше, то это нужно отметить.

Наконец, мы делаем getRangeValue для листа цели и ищем совпадениеномер недели в столбце A И сокращенное название продукта в столбце B. Это дает координаты setValue для нового значения, отслеживаемого из OnEdit.


 function OnEdit(e) {

    // Update relevant Outputsheets on changes in Category sheets

    var ss = SpreadsheetApp.getActiveSpreadsheet();

    // Establish variables

    var s1 = "Category1";
    var s2 = "Category2";
    var s3 = "Category3";
    var tsuffix = "Output";
    //Logger.log("Sheet information");//DEBUG
    //Logger.log("The sheets to track are s1= "+s1+", s2 = "+s2+", and s3 = "+s3+", and the Output suffix is "+tsuffix+". For example s1output = "+s1+tsuffix);// DEBUG

    var TargetSheet = "";
    var weekscolumnstart = 6; // Column F
    var startrow = 10; // applies to the Source sheet
    var rowsperProduct = 8; // applies to the source sheet
    var changedfield = 0;
    var changedfieldname = "";
    var n = 0;

    // Collect data from the event
    var range = e.range;
    var oldValue = e.oldValue;
    var value = e.value
    var source = e.source;
    var sheet = source.getActiveSheet();
    var ssname = sheet.getName();
    // Logger.log("Range: "+range.getA1Notation()+", old value = "+oldValue+", new value = "+value+", source = "+source+", ss = "+sheet+", sheet name = "+ssname); //DEBUG

    // get the co-ordinates of the change
    var SourceRow = range.getRow();
    var SourceColumn = range.getColumn();
    // Logger.log("the Column is "+SourceColumn+", and the Row is "+SourceRow);// DEBUG


    // the weeks range to the right, from column F (va = weekscolumnstart). So by knowing the column number of the even, we can calculate the week number that applied to the change.
    var weeknumber = (SourceColumn - weekscolumnstart + 1);

    switch (ssname) { // the field references are used in a GetValue statement where the column is a reference to a specific column 
        case s1:
            TargetSheet = s1 + tsuffix;
            //Logger.log("The Source sheet was "+ssname+", so the Target sheet is "+TargetSheet);// DEBUG
            break;
        case s2:
            TargetSheet = s2 + tsuffix;
            //Logger.log("The Source sheet was "+ssname+", so the Target sheet is "+TargetSheet);// DEBUG
            break;
        case s3:
            TargetSheet = s3 + tsuffix;
            //Logger.log("The Source sheet was "+ssname+", so the Target sheet is "+TargetSheet);// DEBUG
            break;
        default:
            TargetSheet = 0;
            //Logger.log("The change was made in a sheet that we don't need to track.");
    } // end switch


    // get product and other change information if the change is on a tracked sheet and in a relevant column.
    // evalue for the event on a non-relevant sheet or in a non-relevant column
    if (TargetSheet == 0 || weeknumber <= 0) {
        // do nothing 
    } else {
        //Logger.log("before calculating line number; the TargetSheet is "+TargetSheet);
        // The source has eight rows of data per Product; there is no predictability about which one of the eight will be chnaged for a given product.
        // However the sequence of all the rows follows a mathenmatical sequence, so by knowing the row, it is possible to determine the product grouping
        // And by knowing the product grouping, it is possible to determine the first row of the product group.
        // 
        // The formula for the position of a number n a mathematical sequence is: = an=a1+d(n-1)
        // where an = the "nth" number in the sequence (equates to the nth Product); a1 = the start row (var=startrow); d = difference between each group (var=rowsperProduct) and n=the actual row number.
        // In the first instance we know the row number from the event data, so we work backwards to solve for the position of that number in the sequence.
        // 
        // 1) calculate the starting row for this product
        // 2) (Row number - starting row) divided by rowsperProduct) plus one.
        // 3) There's only a one-in eight chance that it is an integer, so round down to get first row of this product sequence
        // 4) Then we work forwards; since we know the nth number, we can calculate the row number for the first row for that product.
        // 5) starting row plus (rowsperproduct x (seqwuence number minus 1))
        // By knowing the first row in the product sequence, and the row number that was chnaged, we can calculate which data set was chnaged.
        var productseq = (((SourceRow - startrow) / rowsperProduct) + 1);
        var productseqround = Math.floor(productseq);
        var productline = (startrow + (rowsperProduct * (productseqround - 1)));
        //Logger.log("the row number is "+SourceRow+", but the sequence number for this product is "+productseqround+", and the startrow for this product group = "+productline); //DEBUG

        // identify the field that has changed
        // Source Row number less Productline 
        // if 0 = Catalogue
        // if 1= Display
        // if 3 = ESP
        // if 6 = Mechanic

        var LineNumber = (SourceRow - productline);
        //Logger.log("the calculated Line number = "+LineNumber); //DEBUG

        switch (LineNumber) { // the field references are used in a GetValue statement where the column is a reference to a specific column 
            case 0:
                changedfield = 3;
                changedfieldname = "Catalogue";
                //Logger.log("the changed field was "+changedfieldname); // DEBUG
                break;
            case 1:
                changedfield = 4;
                changedfieldname = "Display";
                //Logger.log("the changed field was "+changedfieldname); // DEBUG
                break;
            case 3:
                changedfield = 5;
                changedfieldname = "ESP";
                //Logger.log("the changed field was "+changedfieldname); // DEBUG
                break;
            case 6:
                changedfield = 6;
                changedfieldname = "Mechanic";
                //Logger.log("the changed field was "+changedfieldname); // DEBUG
                break;
            default:
                //Logger.log("the changed field was none of the above");
                changedfield = 0;
        } //end switch

    } //end if


    // OK, let's get this party started..
    // evaluate the sheet
    if (TargetSheet == 0) {
        //Logger.log("Do nothing because it's not on a sheet we need to worry about"); //DEBUG
    }
    // evaluate the week applying to the change
    else if (weeknumber <= 0) {
        //Logger.log("whatever was changed wasn't one of the key fields"); //DEBUG
    }
    //evaluate the changed field
    else if (changedfield == 0) {
        //Logger.log("Do nothing because it's not a field that we're not worried about"); //DEBUG
    }
    // looks OK to go ahead  
    else {
        //Logger.log("the field was changed for week# "+weeknumber+", lets find the product");

        // trim the Product Code for searching on the Output Sheet  
        var LongProdName = sheet.getRange(productline, 4).getValue();
        var Prodedit = LongProdName.substring(11, (LongProdName.length));
        var ShortProdName = Prodedit.trim();
        //Logger.log("the Product Name is "+LongProdName+", shortened to: "+ShortProdName);// DEBUG

        // test for existence of the TargetSheet  
        var sheeterror = 1; // use this variable as the canary in the coal mine. Set to 1, prima facie error
        var target = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TargetSheet);
        if (target != null) { // test for exitentce of target; 
            sheeterror = 0; // if target sheet exists, then set sheeterror to zero; that is, sound the all clear
        }
        if (sheeterror != 0) { // now test for a sheet erorr; anything other than zero means there is a problem
            SpreadsheetApp.getUi().alert("WARNING#1: \n\n The Output Sheet: " + TargetSheet + " does NOT exist.\n\n Product: " + LongProdName + ", \nWeek: " + weeknumber + ",\nField: " + changedfieldname + ", \nold value = " + oldValue + " \n new value = " + value + ".\n Date: " + (new Date()));
            // Logger.log("ERROR: The Outout sheet:" + TargetSheet + " doesn't exist. Data changed on sheet:" + ssname + ", Product: " + LongProdName + ", Week# " + weeknumber + ", Field: " + changedfieldname + ", old value=" + oldValue + ", new value=" + value + ", Date " + (new Date())); //DEBUG
            return false;
        }


        // set the data range for the Output sheet and get the data
        var TargetRange = target.getDataRange();
        var TargetValues = TargetRange.getValues();

        // setup the search string
        // Logger.log("target Range = "+TargetRange.getA1Notation()+", search string = '"+ShortProdName+"', week# is "+weeknumber);  // DEBUG
        // Logger.log("TargetValues length = "+TargetValues.length);
        // so lets find a match
        var outputmatch = 1; // use this variable as the canary in the coal mine for not finding a match. Set to 1 = prima facie error
        for (n = 0; n < TargetValues.length; ++n) {
            // iterate row by row and match the week (Column A) and Name (Column B)
            //Logger.log("n = "+n+", product = "+ShortProdName+", week = "+weeknumber);
            if (TargetValues[n][1] == ShortProdName && TargetValues[n][0] == weeknumber) {

                // when we find the result (row number), add plus one to accout for the array starting at zero.
                var result = n + 1;
                // Logger.log("Found a match, n = "+result);  //DEBUG

                // create the co-ordinates for the output cell
                // row number = result, column = chnagedfield calculated earlier
                // Logger.log("update range: row = "+result+", column = "+changedfield); //DEBUG
                var updatecell = target.getRange(result, changedfield);
                //Logger.log("The update cell is "+updatecell.getA1Notation());  // DEBUG

                // Update the cell for the new value
                updatecell.setValue(value);

                // Fix values for Display/Mechanic if they were updated
                if (changedfieldname == "Display") {
                    var displayvalue = value;
                } else {
                    var displayvalue = TargetValues[n][3];
                }
                if (changedfieldname == "Mechanic") {
                    var mechanicvalue = value;
                } else {
                    var mechanicvalue = TargetValues[n][5];
                }

                // define the join1 parameters  
                var join1 = TargetValues[n][1] + " | " + displayvalue + " | " + mechanicvalue; // Bundle, Display, Mechanic
                // set the range for join 1
                var updatejoin1 = target.getRange(result, 7);
                // update join1
                updatejoin1.setValue(join1);

                // define the join2 parameters  
                var join2 = displayvalue + " | " + TargetValues[n][1] + " | " + mechanicvalue; // Display, Bundle, Mechanic
                // set the range for join 2
                var updatejoin2 = target.getRange(result, 8);
                // update join2
                updatejoin2.setValue(join2);

                // the outputmatch value to zero 
                outputmatch = 0;
                //Logger.log("The update cell is "+updatecell.getA1Notation()+", and the new value is "+ value); //DEBUG
                //Logger.log("SUMMARY: Data changed on sheet:" + ssname + ", saved to Output sheet:" + TargetSheet + ", range: " + range.getA1Notation() + ", Product: " + LongProdName + ", Week# " + weeknumber + ", Field: " + changedfieldname + ", old value=" + oldValue + ", new value=" + value + ", Date " + (new Date())); //DEBUG
                return false;

            }

        } // end for n
        if (outputmatch != 0) { // now test for a faliure to update the output sheet; anything other than zero means there is a problem
            // create an error message if we were unable to find a match and could not update the output sheet field
            SpreadsheetApp.getUi().alert("WARNING#2: There was an unidentified problem.\n\n Output Sheet: " + TargetSheet + " does NOT appear to have been updated.\n\n Product: " + LongProdName + ", \nWeek: " + weeknumber + ",\nField: " + changedfieldname + ", \nold value = " + oldValue + " \n new value = " + value + ".\n Date: " + (new Date()));
            return false;
        }
    } // end if

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...