VBA to Apps Script cheat sheet
15 common Excel VBA idioms and their Google Apps Script equivalents. Every Apps Script snippet on this page is executed in our test harness and checked against the result VBA produces — several popular guides publish code that doesn't run.
Converting a whole workbook? Check your macros freeLast used row in one column
End(xlUp) looks at one column. sheet.getLastRow() looks at every column, so it gives a different answer when columns have different lengths.
Sub ShowLastRow()
Dim last As Long
last = Cells(Rows.Count, 1).End(xlUp).Row
Range("C1").Value = last
End Subfunction ShowLastRow() {
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('C1').setValue(lastRowInColumn_(sheet, 1));
}
// Last non-empty row in one column (1 if the column is empty).
function lastRowInColumn_(sheet, col) {
const values = sheet.getRange(1, col, Math.max(sheet.getLastRow(), 1), 1).getValues();
for (let r = values.length - 1; r >= 0; r--) if (values[r][0] !== '') return r + 1;
return 1;
}✓ Tested: C1 = 2
Loop over rows fast (read once, write once)
Each getValue/setValue call is a round trip that takes tens of milliseconds. Read the block into an array, change it, and write it back once.
Sub AddTax()
Dim i As Long
For i = 2 To 4
Cells(i, 2).Value = Int(Cells(i, 1).Value * 1.1)
Next i
End Subfunction AddTax() {
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getRange(2, 1, 3, 2);
const rows = range.getValues().map(([price]) => [price, Math.floor(price * 1.1)]);
range.setValues(rows);
}✓ Tested: B2 = 110 · B3 = 275 · B4 = 1098
MsgBox
Message boxes become dialogs from SpreadsheetApp.getUi().
Sub Hello()
MsgBox "Done"
End Subfunction Hello() {
SpreadsheetApp.getUi().alert('Done');
}✓ Tested: Done
MsgBox with Yes / No
Compare the result with ui.Button.YES instead of vbYes.
Sub ConfirmClear()
If MsgBox("Clear the sheet?", vbYesNo) = vbYes Then
Range("A1").Value = ""
End If
End Subfunction ConfirmClear() {
const ui = SpreadsheetApp.getUi();
if (ui.alert('Confirm', 'Clear the sheet?', ui.ButtonSet.YES_NO) === ui.Button.YES) {
SpreadsheetApp.getActiveSheet().getRange('A1').setValue('');
}
}✓ Tested: A1 = "keep me"
InputBox (and Cancel)
InputBox returns "" on Cancel; ui.prompt tells you which button was pressed.
Sub AskName()
Dim s As String
s = InputBox("Your name?")
If s = "" Then Exit Sub
Range("A1").Value = s
End Subfunction AskName() {
const ui = SpreadsheetApp.getUi();
const res = ui.prompt('Your name?');
if (res.getSelectedButton() !== ui.Button.OK || res.getResponseText() === '') return;
SpreadsheetApp.getActiveSheet().getRange('A1').setValue(res.getResponseText());
}✓ Tested: A1 = "Aiko"
Worksheets("Name") and missing sheets
getSheetByName returns null instead of raising an error, so check it where VBA would have stopped.
Sub CopyTotal()
Worksheets("Report").Range("A1").Value = Worksheets("Data").Range("B2").Value
End Subfunction CopyTotal() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const data = ss.getSheetByName('Data');
const report = ss.getSheetByName('Report');
if (!data || !report) throw new Error('Sheet "Data" or "Report" not found');
report.getRange('A1').setValue(data.getRange('B2').getValue());
}✓ Tested: A1 = 42
Round (banker's rounding)
VBA's Round(2.5) is 2 and Round(3.5) is 4. Math.round rounds halves up, so results change silently.
Sub RoundDemo()
Range("A1").Value = Round(2.5)
Range("A2").Value = Round(3.5)
Range("A3").Value = Round(2.345, 2)
End Subfunction RoundDemo() {
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A1:A3').setValues([[vbaRound_(2.5)], [vbaRound_(3.5)], [vbaRound_(2.345, 2)]]);
}
// VBA Round: halves go to the nearest even digit.
function vbaRound_(value, digits = 0) {
const f = Math.pow(10, digits);
const x = Number((value * f).toPrecision(15));
const r = Math.round(x);
return (Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r) / f;
}✓ Tested: A1 = 2 · A2 = 4 · A3 = 2.34
Int, Fix and Mod with negatives and decimals
Int rounds down, Fix truncates toward zero, and Mod rounds both operands to integers first (12.6 Mod 5 = 3).
Sub MathDemo()
Range("A1").Value = Int(-2.5)
Range("A2").Value = Fix(-2.5)
Range("A3").Value = 12.6 Mod 5
End Subfunction MathDemo() {
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A1:A3').setValues([[Math.floor(-2.5)], [Math.trunc(-2.5)], [vbaMod_(12.6, 5)]]);
}
function vbaMod_(a, b) {
const toInt = (x) => (Math.abs(x % 1) === 0.5 ? 2 * Math.round(x / 2) : Math.round(x));
return toInt(a) % toInt(b);
}✓ Tested: A1 = -3 · A2 = -2 · A3 = 3
Left, Mid, Right, InStr
VBA positions start at 1 and InStr returns 0 when nothing is found; JavaScript starts at 0 and indexOf returns -1.
Sub StringDemo()
Dim s As String: s = "INV-2026-001"
Range("A1").Value = Left(s, 3)
Range("A2").Value = Mid(s, 5, 4)
Range("A3").Value = Right(s, 3)
Range("A4").Value = InStr(s, "2026")
Range("A5").Value = InStr(s, "X")
End Subfunction StringDemo() {
const s = 'INV-2026-001';
const out = [
s.slice(0, 3),
s.substr(5 - 1, 4), // Mid(s, 5, 4): start is 1-based
s.slice(-3),
s.indexOf('2026') + 1, // InStr: 1-based, 0 when missing
s.indexOf('X') + 1,
];
SpreadsheetApp.getActiveSheet().getRange(1, 1, out.length, 1).setValues(out.map((v) => [v]));
}✓ Tested: A1 = "INV" · A2 = "2026" · A3 = "001" · A4 = 5 · A5 = 0
Format a date
Use Utilities.formatDate with the spreadsheet's time zone. Months are MM (mm means minutes).
Sub DateLabel()
Range("A1").Value = "Issued " & Format(DateSerial(2026, 4, 1), "yyyy/mm/dd")
End Subfunction DateLabel() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const d = new Date(2026, 3, 1); // months are 0-based in JavaScript
const text = Utilities.formatDate(d, ss.getSpreadsheetTimeZone(), 'yyyy/MM/dd');
ss.getActiveSheet().getRange('A1').setValue('Issued ' + text);
}✓ Tested: A1 = "Issued 2026/04/01"
Month end and adding months
DateSerial(y, m + 1, 0) is new Date(y, m, 0) in JavaScript. DateAdd("m") clamps Jan 31 + 1 month to Feb 28; plain setMonth would give Mar 3.
Sub Dates()
Range("A1").Value = Format(DateSerial(2026, 3, 0), "yyyy/mm/dd")
Range("A2").Value = Format(DateAdd("m", 1, DateSerial(2026, 1, 31)), "yyyy/mm/dd")
End Subfunction Dates() {
const tz = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone();
const fmt = (d) => Utilities.formatDate(d, tz, 'yyyy/MM/dd');
SpreadsheetApp.getActiveSheet().getRange('A1:A2').setValues([
[fmt(new Date(2026, 2, 0))],
[fmt(addMonths_(new Date(2026, 0, 31), 1))],
]);
}
// Like VBA DateAdd("m"): keeps the day, clamped to the target month's length.
function addMonths_(date, n) {
const d = new Date(date.getFullYear(), date.getMonth() + n, 1);
const last = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
d.setDate(Math.min(date.getDate(), last));
return d;
}✓ Tested: A1 = "2026/02/28" · A2 = "2026/02/28"
Scripting.Dictionary → Map
Exists → has, Item → get/set, Count → size, Keys → [...map.keys()].
Sub TotalsByCustomer()
Dim d As Object, i As Long
Set d = CreateObject("Scripting.Dictionary")
For i = 2 To 5
d(Cells(i, 1).Value) = d(Cells(i, 1).Value) + Cells(i, 2).Value
Next i
Range("D1").Value = d("A")
Range("D2").Value = d.Count
End Subfunction TotalsByCustomer() {
const sheet = SpreadsheetApp.getActiveSheet();
const totals = new Map();
for (const [name, amount] of sheet.getRange(2, 1, 4, 2).getValues()) {
totals.set(name, (totals.get(name) || 0) + amount);
}
sheet.getRange('D1:D2').setValues([[totals.get('A')], [totals.size]]);
}✓ Tested: D1 = 17 · D2 = 3
On Error GoTo → try/catch (and division by zero)
JavaScript does not raise an error on division by zero; it returns Infinity. Check the divisor yourself where VBA relied on the error.
Sub UnitPrice()
On Error GoTo Failed
Range("C1").Value = Range("A1").Value / Range("B1").Value
Exit Sub
Failed:
Range("C1").Value = "error"
End Subfunction UnitPrice() {
const sheet = SpreadsheetApp.getActiveSheet();
try {
const [[total, qty]] = sheet.getRange('A1:B1').getValues();
if (qty === 0 || qty === '') throw new Error('Division by zero');
sheet.getRange('C1').setValue(total / qty);
} catch (e) {
sheet.getRange('C1').setValue('error');
}
}✓ Tested: C1 = "error"
Delete rows in a loop
Delete from the bottom up, or rebuild the data with a filter and write it back once (faster).
Sub RemoveDone()
Dim i As Long
For i = 5 To 2 Step -1
If Cells(i, 2).Value = "done" Then Rows(i).Delete
Next i
End Subfunction RemoveDone() {
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getRange(2, 1, 4, 2);
const keep = range.getValues().filter(([, status]) => status !== 'done');
range.clearContent();
if (keep.length) sheet.getRange(2, 1, keep.length, 2).setValues(keep);
}✓ Tested: A2 = "b" · A3 = "d" · A4 = ""
Debug.Print
Use console.log; the output appears in Apps Script's Executions log.
Sub Trace()
Debug.Print "rows: " & 3
End Subfunction Trace() {
console.log('rows: ' + 3);
}✓ Tested: rows: 3
Converting a whole workbook? Check your macros free
Snippets are MIT-licensed: copy them freely.