At the end, there’s one single .gs file you can paste into your Apps Script project. It creates the sheets, seeds data, and lets you run each exercise from a custom menu.
How to get set up (once)
- Open (or create) a Google Sheet.
- Extensions ➝ Apps Script.
- Delete any placeholder code and paste in the ONE full code file below.
- Click Save (give the project any name).
- Run ex1_setup_sales_sheet() once to seed the sample data (or use Exercises menu added by the script).
- Then run each exercise function in order, or use Exercises → Run All Exercises.
Exercise 1 — Create & Seed a “Sales” sheet
Objective: Learn to create a sheet, write headers/rows, and format columns.
You’ll accomplish: A new Sales sheet with sample rows: Date, Region, Salesperson, Product, Units, UnitPrice, Total.
Steps
- Run ex1_setup_sales_sheet().
- Open the Sales sheet—data will be ready to use in the next exercises.
How it works (high-level)
- Creates or clears a sheet named Sales.
- Writes headers and 15+ sample rows.
- Calculates Total = Units × UnitPrice in the script before writing.
- Applies date and currency formats; freezes header row.
Exercise 2 — Totals by Product (write a summary)
Objective: Read ranges, aggregate data in JavaScript, and write a summary table.
You’ll accomplish: A Totals sheet with Product → Total Sales, sorted desc.
Steps
- Run ex2_totals_by_product().
- Check the Totals sheet to see the summary.
How it works
- Reads all data from Sales.
- Builds a product → sum map in JS.
- Writes a two-column table and formats as currency.
- Sorts totals descending.
Exercise 3 — Append a New Sale Row
Objective: Append a new row with computed values and a timestamp.
You’ll accomplish: Adds a new sale record to Sales (timestamped today).
Steps
- Run ex3_append_sale_row().
- See the new row at the bottom of Sales.
How it works
- Picks a sample Region/Salesperson/Product, Units, and UnitPrice.
- Computes Total and appends the row.
Exercise 4 — Highlight the Top 3 Sales Rows
Objective: Work with arrays from a sheet and set background colors programmatically.
You’ll accomplish: The three highest-Total rows in Sales are highlighted light green.
Steps
- Run ex4_highlight_top3_sales().
- Notice top 3 by Total shaded.
How it works
- Reads all rows, finds top 3 Total values, and sets background colors only on those rows.
- Resets others to white.
Exercise 5 — Standardize Salesperson Names (Find & Replace)
Objective: Use TextFinder and dictionary-based standardization.
You’ll accomplish: Fixes inconsistent names (e.g., “Alyce” → “Alice”, “bob” → “Bob”).
Steps
- Run ex5_standardize_salesperson_names().
- Check Salesperson column in Sales—inconsistencies are fixed.
How it works
- Uses a mapping dictionary of incorrect → correct spellings.
- For each mapping, uses createTextFinder over the Salesperson column to replace all.
Exercise 6 — Remove Duplicate Sales
Objective: Identify duplicates by a composite key and delete them.
You’ll accomplish: Deduped Sales (keeps first occurrence).
Steps
- Run ex6_remove_duplicate_sales().
- Rows that are exact repeats by Date+Salesperson+Product are removed.
How it works
- Builds a key for each row (date string + salesperson + product).
- Tracks seen keys and deletes later duplicates (bottom-up to keep row indexes stable).
Exercise 7 — Create a Filtered “HighValue” Sheet
Objective: Filter in code and write results to a new sheet.
You’ll accomplish: A HighValue sheet containing only rows with Total ≥ 500.
Steps
- Run ex7_create_high_value_sheet().
- Open HighValue to see the filtered subset.
How it works
- Filters the Sales array for totals ≥ threshold.
- Writes a new table with headers; keeps formats consistent.
Exercise 8 — Insert a Chart Programmatically
Objective: Create a chart with the Apps Script Charts API.
You’ll accomplish: A column chart of Totals by Product embedded on the Totals sheet.
Steps
- Run ex8_insert_totals_chart().
- Open Totals—a column chart appears under the summary table.
How it works
- Uses newChart() builder on the Totals sheet.
- Binds the chart to the product/total range.
- Positions it below the data.
Exercise 9 — Custom Menu & Custom Functions
Objective: Add a menu and define functions usable directly in cells.
You’ll accomplish:
- A menu Exercises ➝ run any exercise or Run All.
- Custom functions: TAX_AMOUNT(amount, rate) and AVERAGE_IGNORE_ZERO(range).
Steps
- Reload your spreadsheet so onOpen() runs and menu appears.
- In any cell, try: =TAX_AMOUNT(100, 0.13) → 13
- Try: =AVERAGE_IGNORE_ZERO(A2:A100) to ignore zeros.
How it works
- onOpen() builds the Exercises menu.
- Custom functions are top-level and return simple values/arrays to the cell.
Exercise 10 — Data Validation + onEdit Auto-Calc
Objective: Create a dropdown and calculate row totals automatically on edit.
You’ll accomplish:
- An Input sheet with a Product dropdown and Units/UnitPrice fields.
- When you type Units or UnitPrice, Total updates instantly via onEdit.
Steps
- Run ex10_data_validation_with_onedit().
- On Input, choose a product, type Units and UnitPrice—Total fills in.
How it works
- Creates list validation on Product from known products.
- onEdit(e) watches edits on Input; when Units/UnitPrice changes, it sets Total = Units × UnitPrice for that row.
One complete .gs file (paste this whole thing)
/***** Utilities *****/
function get_or_create_sheet_(name) {
const ss = SpreadsheetApp.getActive();
let sh = ss.getSheetByName(name);
if (!sh) sh = ss.insertSheet(name);
return sh;
}
function set_headers_(sh, headers) {
sh.clear();
sh.getRange(1, 1, 1, headers.length).setValues([headers]);
sh.setFrozenRows(1);
}
function seed_sales_data_if_needed_() {
const sh = get_or_create_sheet_(“Sales”);
const headers = [“Date”, “Region”, “Salesperson”, “Product”, “Units”, “UnitPrice”, “Total”];
if (sh.getLastRow() < 2) {
set_headers_(sh, headers);
const rows = [
// Date (YYYY, M-1, D), Region, Salesperson (some intentionally messy), Product, Units, UnitPrice
[new Date(2025,0,5), “East”, “Alice”, “Widget”, 5, 20],
[new Date(2025,0,6), “West”, “bob”, “Gizmo”, 12, 35],
[new Date(2025,0,7), “South”, “Carmen”, “Doodad”, 3, 120],
[new Date(2025,0,8), “North”, “Deepa”, “Widget”, 10, 22.5],
[new Date(2025,0,9), “Central”,”Evan”, “Gizmo”, 9, 40],
[new Date(2025,0,10), “East”, “Alyce”, “Widget”, 8, 20],
[new Date(2025,0,11), “West”, “Bob”, “Doodad”, 2, 150],
[new Date(2025,0,12), “South”, “Carmen”, “Gizmo”, 7, 38],
[new Date(2025,0,12), “South”, “Carmen”, “Gizmo”, 7, 38], // duplicate on purpose
[new Date(2025,0,13), “North”, “Deepa”, “Widget”, 14, 21],
[new Date(2025,0,14), “Central”,”Evan”, “Doodad”, 4, 130],
[new Date(2025,0,15), “East”, “Alice”, “Gizmo”, 11, 36.5],
[new Date(2025,0,16), “West”, “Bob”, “Widget”, 6, 23],
[new Date(2025,0,17), “South”, “Carmen”, “Doodad”, 5, 125],
[new Date(2025,0,18), “North”, “Deepa”, “Gizmo”, 13, 34],
[new Date(2025,0,19), “Central”,”Evan”, “Widget”, 9, 22]
];
const out = rows.map(r => {
const total = Number(r[4]) * Number(r[5]);
return [r[0], r[1], r[2], r[3], r[4], r[5], total];
});
sh.getRange(2, 1, out.length, out[0].length).setValues(out);
// Formats
sh.getRange(“A2:A”).setNumberFormat(“yyyy-mm-dd”);
sh.getRange(“F2:G”).setNumberFormat(“$#,##0.00”);
sh.autoResizeColumns(1, 7);
}
return sh;
}
/***** Exercise 1 *****/
function ex1_setup_sales_sheet() {
const sh = get_or_create_sheet_(“Sales”);
sh.clear();
seed_sales_data_if_needed_();
}
/***** Exercise 2 *****/
function ex2_totals_by_product() {
const sales = seed_sales_data_if_needed_();
const values = sales.getDataRange().getValues();
const headers = values.shift(); // remove headers
const idxProduct = headers.indexOf(“Product”);
const idxTotal = headers.indexOf(“Total”);
const totalsMap = {};
values.forEach(r => {
const p = r[idxProduct];
const t = Number(r[idxTotal]) || 0;
totalsMap[p] = (totalsMap[p] || 0) + t;
});
const totals = Object.entries(totalsMap).map(([p, t]) => [p, t]);
totals.sort((a, b) => b[1] – a[1]);
const sh = get_or_create_sheet_(“Totals”);
set_headers_(sh, [“Product”, “TotalSales”]);
if (totals.length) {
sh.getRange(2, 1, totals.length, 2).setValues(totals);
sh.getRange(2, 2, totals.length, 1).setNumberFormat(“$#,##0.00”);
sh.autoResizeColumns(1, 2);
}
}
/***** Exercise 3 *****/
function ex3_append_sale_row() {
const sh = seed_sales_data_if_needed_();
const regions = [“East”, “West”, “North”, “South”, “Central”];
const salespeople = [“Alice”, “Bob”, “Carmen”, “Deepa”, “Evan”];
const products = [“Widget”, “Gizmo”, “Doodad”];
// Simple demo data
const region = regions[Math.floor(Math.random() * regions.length)];
const person = salespeople[Math.floor(Math.random() * salespeople.length)];
const product = products[Math.floor(Math.random() * products.length)];
const units = Math.ceil(5 + Math.random() * 10); // 5..15
const unitPrice = product === “Doodad” ? 120 + Math.random() * 40 :
product === “Gizmo” ? 30 + Math.random() * 10 :
20 + Math.random() * 5;
const total = units * unitPrice;
const row = [new Date(), region, person, product, units, unitPrice, total];
sh.appendRow(row);
const last = sh.getLastRow();
sh.getRange(last, 1, 1, 1).setNumberFormat(“yyyy-mm-dd”);
sh.getRange(last, 6, 1, 2).setNumberFormat(“$#,##0.00”);
}
/***** Exercise 4 *****/
function ex4_highlight_top3_sales() {
const sh = seed_sales_data_if_needed_();
const lastRow = sh.getLastRow();
if (lastRow < 3) return;
const range = sh.getRange(2, 1, lastRow – 1, 7);
const values = range.getValues();
const headers = sh.getRange(1, 1, 1, 7).getValues()[0];
const idxTotal = headers.indexOf(“Total”);
// Collect totals with row offsets
const totals = values.map((r, i) => ({ idx: i, total: Number(r[idxTotal]) || 0 }));
totals.sort((a, b) => b.total – a.total);
const topCount = Math.min(3, totals.length);
const topIdx = new Set(totals.slice(0, topCount).map(o => o.idx));
// Build background matrix
const bg = values.map((_, i) => {
const color = topIdx.has(i) ? “#d8f7d4” : “#ffffff”; // light green or white
return new Array(7).fill(color);
});
range.setBackgrounds(bg);
}
/***** Exercise 5 *****/
function ex5_standardize_salesperson_names() {
const sh = seed_sales_data_if_needed_();
const headers = sh.getRange(1, 1, 1, sh.getLastColumn()).getValues()[0];
const idxSalesperson = headers.indexOf(“Salesperson”) + 1; // 1-based
const fixes = {
“Alyce”: “Alice”,
“alyce”: “Alice”,
“bob”: “Bob”,
“Bob “: “Bob”,
” Bob”: “Bob”
};
const colRange = sh.getRange(2, idxSalesperson, sh.getLastRow() – 1, 1);
Object.keys(fixes).forEach(bad => {
colRange.createTextFinder(bad).matchCase(false).matchEntireCell(false).replaceAllWith(fixes[bad]);
});
}
/***** Exercise 6 *****/
function ex6_remove_duplicate_sales() {
const sh = seed_sales_data_if_needed_();
const range = sh.getDataRange();
const values = range.getValues();
const headers = values.shift();
const idxDate = headers.indexOf(“Date”);
const idxSalesperson = headers.indexOf(“Salesperson”);
const idxProduct = headers.indexOf(“Product”);
const seen = new Set();
const toDelete = [];
values.forEach((r, i) => {
const d = r[idxDate] instanceof Date ? r[idxDate] : new Date(r[idxDate]);
const dateKey = Utilities.formatDate(d, Session.getScriptTimeZone(), “yyyy-MM-dd”);
const key = [dateKey, r[idxSalesperson], r[idxProduct]].join(“|”);
if (seen.has(key)) {
toDelete.push(i + 2); // sheet row index
} else {
seen.add(key);
}
});
// Delete from bottom up
toDelete.sort((a, b) => b – a).forEach(row => sh.deleteRow(row));
}
/***** Exercise 7 *****/
function ex7_create_high_value_sheet() {
const src = seed_sales_data_if_needed_();
const dst = get_or_create_sheet_(“HighValue”);
const headers = src.getRange(1, 1, 1, 7).getValues()[0];
const idxTotal = headers.indexOf(“Total”);
const values = src.getDataRange().getValues();
values.shift(); // drop headers
const threshold = 500;
const filtered = values.filter(r => Number(r[idxTotal]) >= threshold);
set_headers_(dst, headers);
if (filtered.length) {
dst.getRange(2, 1, filtered.length, headers.length).setValues(filtered);
dst.getRange(“A2:A”).setNumberFormat(“yyyy-mm-dd”);
dst.getRange(“F2:G”).setNumberFormat(“$#,##0.00”);
dst.autoResizeColumns(1, headers.length);
}
}
/***** Exercise 8 *****/
function ex8_insert_totals_chart() {
ex2_totals_by_product(); // ensure Totals exists
const sh = get_or_create_sheet_(“Totals”);
const last = sh.getLastRow();
if (last < 2) return;
// Remove existing charts for a clean demo (optional)
sh.getCharts().forEach(c => sh.removeChart(c));
// Data range: Product (A), TotalSales (B)
const dataRange = sh.getRange(1, 1, last, 2);
const chart = sh.newChart()
.setChartType(Charts.ChartType.COLUMN)
.addRange(dataRange)
.setOption(“title”, “Total Sales by Product”)
.setPosition( last + 2, 1, 0, 0)
.build();
sh.insertChart(chart);
}
/***** Exercise 9 *****/
function onOpen() {
// Build a menu to run exercises quickly
SpreadsheetApp.getUi()
.createMenu(“Exercises”)
.addItem(“1) Setup Sales Sheet”, “ex1_setup_sales_sheet”)
.addItem(“2) Totals by Product”, “ex2_totals_by_product”)
.addItem(“3) Append Sale Row”, “ex3_append_sale_row”)
.addItem(“4) Highlight Top 3”, “ex4_highlight_top3_sales”)
.addItem(“5) Standardize Names”, “ex5_standardize_salesperson_names”)
.addItem(“6) Remove Duplicates”, “ex6_remove_duplicate_sales”)
.addItem(“7) HighValue Sheet”, “ex7_create_high_value_sheet”)
.addItem(“8) Insert Totals Chart”, “ex8_insert_totals_chart”)
.addItem(“10) Setup Input & onEdit”, “ex10_data_validation_with_onedit”)
.addSeparator()
.addItem(“Run All Exercises”, “run_all_exercises”)
.addToUi();
}
// Custom functions (can be used in cells)
function TAX_AMOUNT(amount, rate) {
amount = Number(amount); rate = Number(rate);
if (isNaN(amount) || isNaN(rate)) return “”;
return amount * rate;
}
function AVERAGE_IGNORE_ZERO(range) {
const flat = (Array.isArray(range) ? range.flat() : [range]).map(Number);
const nonZero = flat.filter(n => !isNaN(n) && n !== 0);
if (!nonZero.length) return 0;
return nonZero.reduce((a, b) => a + b, 0) / nonZero.length;
}
/***** Exercise 10 *****/
function ex10_data_validation_with_onedit() {
const input = get_or_create_sheet_(“Input”);
const headers = [“Product”, “Units”, “UnitPrice”, “Total”, “Region”, “Salesperson”];
set_headers_(input, headers);
input.setColumnWidths(1, headers.length, 140);
// Build product list from Sales (or fallback)
const sales = seed_sales_data_if_needed_();
const vals = sales.getRange(2, 4, sales.getLastRow() – 1, 1).getValues().flat(); // Product col
const products = Array.from(new Set(vals.length ? vals : [“Widget”,”Gizmo”,”Doodad”]));
// Apply dropdown to Product column
const rule = SpreadsheetApp.newDataValidation()
.requireValueInList(products, true)
.setAllowInvalid(false)
.build();
input.getRange(2, 1, 100, 1).setDataValidation(rule);
// Formats
input.getRange(“C2:C”).setNumberFormat(“$#,##0.00”);
input.getRange(“D2:D”).setNumberFormat(“$#,##0.00”);
}
function onEdit(e) {
try {
const sh = e.range.getSheet();
if (sh.getName() !== “Input”) return;
const row = e.range.getRow();
const col = e.range.getColumn();
if (row < 2) return;
// Columns: A=Product, B=Units, C=UnitPrice, D=Total
const units = Number(sh.getRange(row, 2).getValue() || 0);
const price = Number(sh.getRange(row, 3).getValue() || 0);
if (col === 2 || col === 3) {
const total = units * price;
sh.getRange(row, 4).setValue(total).setNumberFormat(“$#,##0.00”);
}
} catch (err) {
// Swallow errors in simple trigger
}
}
/***** Convenience: run all in order *****/
function run_all_exercises() {
ex1_setup_sales_sheet();
ex2_totals_by_product();
ex3_append_sale_row();
ex4_highlight_top3_sales();
ex5_standardize_salesperson_names();
ex6_remove_duplicate_sales();
ex7_create_high_value_sheet();
ex8_insert_totals_chart();
ex10_data_validation_with_onedit();
}
Tips & Notes
- If you see an authorization prompt the first time you run a function, grant permissions.
- onOpen and onEdit are simple triggers and run automatically (no manual install needed). If the menu doesn’t appear, refresh the sheet.
- You can safely re-run ex1_setup_sales_sheet() to reset the Sales sheet to a known state.
- All function names avoid special characters—use them directly in the editor or via the menu.
If you want, I can adapt these to your exact teaching style (e.g., rename columns, add more products, or switch to a different dataset like “Students & Grades”).
