VBAMigrate

Excelマクロをスプレッドシートで動かす方法 ― VBA→GAS変換の実例つき

「Google Workspace に移ったら、請求書作成ボタンが動かなくなった」。この記事では、Excel の VBA マクロを Google スプレッドシートで動かすための手順を、実際の業務マクロ 3 本で説明します。

最初に結論です。スプレッドシートは VBA を実行できません。 マクロ付きブック(.xlsm)をアップロードしてスプレッドシートで開いても、表と数式は移りますがマクロは動きません。動かすには、Google Apps Script(GAS)というJavaScriptベースの言語に書き換える必要があります。

なぜ今、マクロの移行が必要になっているのか

理由は大きく二つあります。

  1. Excel 側でもマクロが止まりやすくなった。 Microsoft は「インターネットからファイル内のマクロをブロックするように Office アプリケーションの既定の動作を変更」しました。メール添付やダウンロードしたファイルのマクロは、既定で実行されません(Microsoft Learn)。社外に Excel ツールを配っている人ほど影響を受けます。実際にクラウドソーシングでも、税理士向けの VBA ツールを「WINDOWSのマクロプログラムのブロック」を理由に GAS へ移したいという依頼が出ています(ランサーズの募集)。
  2. Google Workspace に移ると、マクロだけが取り残される。 Google Workspace の導入支援を行う rakumo 社は、「マクロが動かない」という声を、スプレッドシートへの移行を阻む「最も大きな要因の一つ」としています(rakumo)。

Google 公式の「Macro Converter」が使えない人が多い理由

Google には VBA を Apps Script に変換する公式アドオン「Macro Converter」があります。ただし利用には Google Workspace Enterprise Plus または Education Plus のアカウントが必要です(Google for Developers)。Business Starter / Standard や個人アカウントでは使えません。Marketplace のレビューにも「誰でも変換できなければ無いのと同じ」という声があります(Marketplace、2026年5月のレビュー)。

また、使える場合でも、ユーザーフォームは変換されず手作業で作り直す必要があり、一部のイベントトリガー(BeforeClose、BeforeSave など)にも対応がありません(Google: 一般的な問題への対処)。

移し替えの基本手順

  1. マクロの棚卸し:どのブックのどのボタンが、どのマクロを呼んでいるか一覧にする
  2. 移せない処理を先に見つける:ユーザーフォーム、Windows API、他の Office アプリ(Outlook、Word)の操作、印刷、ローカルフォルダの読み書きは、そのままでは移せません
  3. GAS に書き換える:拡張機能 → Apps Script を開き、コードを書く(または変換したものを貼る)
  4. ボタンを付け直す:図形に「スクリプトを割り当て」るか、カスタムメニューを作る。Workbook_Open は onOpen に置き換える
  5. 元の Excel と同じデータで結果を比べる:これを省くと、後述の「静かな違い」に気づけません

GAS には 1 回の実行につき 6 分の上限があります(Apps Script の割り当て)。VBA のように 1 セルずつ読み書きするループは、getValues / setValues でまとめて処理する形に変える必要があります。

実例1:請求書作成マクロ

売上明細シートから、指定した顧客の未請求分だけを請求書シートに転記し、小計・消費税(切り捨て)・合計を出して、明細に「済」を付けるマクロです。

Public Const 税率 As Double = 0.1

' 請求書シートのB2に入れた顧客コードで、未請求の売上明細を請求書に転記する
Sub 請求書作成()
    Dim 顧客 As String
    顧客 = Sheet2.Range("B2").Value
    If 顧客 = "" Then
        MsgBox "請求先の顧客コードを入力してください", vbExclamation
        Exit Sub
    End If

    Dim 顧客名 As Variant
    顧客名 = Application.VLookup(顧客, Sheet3.Range("A:B"), 2, False)
    If IsError(顧客名) Then
        MsgBox "顧客コード " & 顧客 & " がマスタにありません", vbCritical
        Exit Sub
    End If
    Sheet2.Range("B3").Value = 顧客名

    Sheet2.Range("A7:D30").ClearContents

    Dim 最終行 As Long, i As Long, 出力行 As Long
    Dim 小計 As Currency
    最終行 = Sheet1.Cells(Sheet1.Rows.Count, 1).End(xlUp).Row
    出力行 = 7
    For i = 2 To 最終行
        With Sheet1
            If .Cells(i, 2).Value = 顧客 And .Cells(i, 6).Value <> "済" Then
                Sheet2.Cells(出力行, 1).Value = .Cells(i, 1).Value
                Sheet2.Cells(出力行, 2).Value = .Cells(i, 3).Value
                Sheet2.Cells(出力行, 3).Value = .Cells(i, 4).Value
                Sheet2.Cells(出力行, 4).Value = .Cells(i, 4).Value * .Cells(i, 5).Value
                小計 = 小計 + Sheet2.Cells(出力行, 4).Value
                .Cells(i, 6).Value = "済"
                出力行 = 出力行 + 1
            End If
        End With
    Next i

    Sheet2.Range("D32").Value = 小計
    Sheet2.Range("D33").Value = Int(小計 * 税率)
    Sheet2.Range("D34").Value = 小計 + Int(小計 * 税率)
    MsgBox Sheet2.Range("B3").Value & " 様の請求書を作成しました(" & 出力行 - 7 & "件)"
End Sub

移すときのつまずきポイント

変換後の Apps Script

// Module1.gs
// 変換元: Excel VBA Module1
// シートの対応: Sheet1 → 「売上明細」、Sheet2 → 「請求書」、Sheet3 → 「顧客マスタ」

const 税率 = 0.1;

// Excel のボタンの代わりに、メニューから実行できるようにします
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('請求書')
    .addItem('請求書作成', '請求書作成')
    .addToUi();
}

// 請求書シートのB2に入れた顧客コードで、未請求の売上明細を請求書に転記する
function 請求書作成() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const ui = SpreadsheetApp.getUi();
  const sh売上 = getSheetOrThrow_(ss, '売上明細');
  const sh請求 = getSheetOrThrow_(ss, '請求書');
  const sh顧客 = getSheetOrThrow_(ss, '顧客マスタ');

  const 顧客 = String(sh請求.getRange('B2').getValue());
  if (顧客 === '') {
    ui.alert('請求先の顧客コードを入力してください');
    return;
  }

  // REVIEW: Excel の VLOOKUP(完全一致・大文字小文字を区別しない)に合わせた照合。ワイルドカード(* ?)は使えません
  const 顧客名 = vlookupExact_(sh顧客, 顧客);
  if (顧客名 === undefined) {
    ui.alert('顧客コード ' + 顧客 + ' がマスタにありません');
    return;
  }
  sh請求.getRange('B3').setValue(顧客名);

  sh請求.getRange('A7:D30').clearContent();

  // A列の最後の入力行(Cells(Rows.Count, 1).End(xlUp).Row 相当)
  const 最終行 = lastRowInColumn_(sh売上, 1);
  const 出力 = [];
  const 済セル = [];
  let 小計 = 0;

  if (最終行 >= 2) {
    const data = sh売上.getRange(2, 1, 最終行 - 1, 6).getValues();
    data.forEach((row, idx) => {
      if (String(row[1]) === 顧客 && row[5] !== '済') {
        const 金額 = row[3] * row[4];
        出力.push([row[0], row[2], row[3], 金額]);
        小計 = toCurrency_(小計 + 金額);
        済セル.push('F' + (idx + 2));
      }
    });
  }

  if (出力.length > 0) {
    // 元のマクロと同じく7行目から書き込みます(24件を超えると31行目以降にも書き込まれます)
    sh請求.getRange(7, 1, 出力.length, 4).setValues(出力);
    // F列は該当したセルだけ「済」にします(他のセルの数式は変更しません)
    sh売上.getRangeList(済セル).setValue('済');
  }

  const 税額 = Math.floor(小計 * 税率);
  sh請求.getRange('D32:D34').setValues([[小計], [税額], [小計 + 税額]]);

  SpreadsheetApp.flush();
  ui.alert(String(顧客名) + ' 様の請求書を作成しました(' + 出力.length + '件)');
}

function getSheetOrThrow_(ss, name) {
  const sh = ss.getSheetByName(name);
  if (!sh) {
    throw new Error(`シート「${name}」が見つかりません。シート名を確認してください。`);
  }
  return sh;
}

// 顧客マスタのA列から完全一致で探し、B列の値を返す(見つからなければ undefined)
function vlookupExact_(sheet, key) {
  const last = sheet.getLastRow();
  if (last === 0) return undefined;
  const vals = sheet.getRange(1, 1, last, 2).getValues();
  const k = key.toLowerCase();
  for (const row of vals) {
    if (row[0] !== '' && String(row[0]).toLowerCase() === k) return row[1];
  }
  return undefined;
}

// 指定列で最後に値が入っている行番号(空なら1)
function lastRowInColumn_(sheet, col) {
  const maxRow = sheet.getLastRow();
  if (maxRow === 0) return 1;
  const vals = sheet.getRange(1, col, maxRow, 1).getValues();
  for (let r = vals.length - 1; r >= 0; r--) {
    if (vals[r][0] !== '' && vals[r][0] !== null) return r + 1;
  }
  return 1;
}

// VBA の Currency 型と同じく小数4桁に丸める(偶数丸め)
function toCurrency_(x) {
  const scaled = x * 10000;
  let r = Math.round(scaled);
  if (Math.abs(scaled % 1) === 0.5 && r % 2 !== 0) r -= 1;
  return r / 10000;
}

同じデータで確認した結果(顧客コード C002、明細 5 行のうち対象 2 行)

セル 期待値 意味
請求書!B3 八百屋B 顧客マスタから取得
請求書!B7 / D7 みかん / 400 5 × 80
請求書!B8 / D8 もも / 999 3 × 333
請求書!D32 / D33 / D34 1399 / 139 / 1538 消費税は切り捨て(Int)
売上明細!F3, F5 済 転記した行だけに付く
メッセージ 八百屋B 様の請求書を作成しました(2件)

存在しない顧客コード(C999)や空欄のときに、元のマクロと同じエラーメッセージが出ることも確認しています。

実例2:受注シートから出荷シートへの転記

「出荷待ち」の受注だけを出荷シートに転記し、転記済みの受注番号は飛ばし、出荷シートがなければ作るマクロです。

' 受注シートの「出荷待ち」を出荷シートへ転記する(転記済みの受注番号はスキップ)
Sub 出荷転記()
    Dim 受注 As Worksheet, 出荷 As Worksheet
    Set 受注 = ThisWorkbook.Worksheets("受注")

    On Error Resume Next
    Set 出荷 = ThisWorkbook.Worksheets("出荷")
    On Error GoTo 0
    If 出荷 Is Nothing Then
        Set 出荷 = ThisWorkbook.Worksheets.Add(After:=受注)
        出荷.Name = "出荷"
        出荷.Range("A1:D1").Value = Array("受注番号", "顧客", "数量", "転記日")
    End If

    Dim 対象() As Long, 件数 As Long
    Dim i As Long, 最終行 As Long
    最終行 = 受注.Cells(受注.Rows.Count, "A").End(xlUp).Row

    For i = 2 To 最終行
        If 受注.Cells(i, "D").Value = "出荷待ち" Then
            Dim 見つかった As Range
            Set 見つかった = 出荷.Columns(1).Find(What:=受注.Cells(i, "A").Value, LookAt:=xlWhole)
            If 見つかった Is Nothing Then
                件数 = 件数 + 1
                ReDim Preserve 対象(1 To 件数)
                対象(件数) = i
            End If
        End If
    Next i

    If 件数 = 0 Then
        MsgBox "転記する受注はありません"
        Exit Sub
    End If

    Dim 書込行 As Long, k As Long
    書込行 = 出荷.Cells(出荷.Rows.Count, 1).End(xlUp).Row + 1
    For k = 1 To 件数
        With 受注.Rows(対象(k))
            出荷.Cells(書込行, 1).Value = .Cells(1, 1).Value
            出荷.Cells(書込行, 2).Value = .Cells(1, 2).Value
            出荷.Cells(書込行, 3).Value = .Cells(1, 3).Value
            出荷.Cells(書込行, 4).Value = 基準日()
        End With
        受注.Cells(対象(k), "D").Value = "出荷済"
        書込行 = 書込行 + 1
    Next k
    MsgBox 件数 & "件を出荷シートに転記しました"
End Sub

Function 基準日() As Date
    基準日 = DateSerial(2026, 11, 10)
End Function

つまずきポイント

変換後の Apps Script

// Module1.gs
// 受注シートの「出荷待ち」を出荷シートへ転記する(転記済みの受注番号はスキップ)
function 出荷転記() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const ui = SpreadsheetApp.getUi();

  const 受注 = ss.getSheetByName('受注');
  if (!受注) {
    throw new Error('シート「受注」が見つかりません');
  }

  let 出荷 = ss.getSheetByName('出荷');
  if (!出荷) {
    // 受注シートの直後に追加(insertSheet の位置は 0 始まり、getIndex は 1 始まり)
    出荷 = ss.insertSheet('出荷', 受注.getIndex());
    出荷.getRange('A1:D1').setValues([['受注番号', '顧客', '数量', '転記日']]);
  }

  // A列の最終行(Cells(Rows.Count, "A").End(xlUp).Row 相当)
  const 最終行 = 列の最終行_(受注, 1);

  // 出荷シートA列の既存の受注番号(ループ前の状態で判定する点は元のマクロと同じ)
  const 既存 = new Set(列の値_(出荷, 1).map(検索キー_));

  const 対象 = [];
  if (最終行 >= 2) {
    const 受注値 = 受注.getRange(2, 1, 最終行 - 1, 4).getValues();
    受注値.forEach((row, idx) => {
      if (row[3] === '出荷待ち') {
        const 番号 = row[0];
        // REVIEW: Excel の Find は空文字で検索すると空白セルを見つけるため、受注番号が空の行は転記されません(元の動作に合わせています)
        if (番号 === '') return;
        if (!既存.has(検索キー_(番号))) {
          対象.push({ 行: idx + 2, 値: row });
        }
      }
    });
  }

  const 件数 = 対象.length;
  if (件数 === 0) {
    ui.alert('転記する受注はありません');
    return;
  }

  const 書込行 = 列の最終行_(出荷, 1) + 1;
  const 日付 = 基準日();
  const 出力 = 対象.map(t => [t.値[0], t.値[1], t.値[2], 日付]);
  出荷.getRange(書込行, 1, 件数, 4).setValues(出力);
  // Excel では日付を書き込むと日付表示になるため、それに合わせて日付書式を設定
  出荷.getRange(書込行, 4, 件数, 1).setNumberFormat('yyyy/mm/dd');

  // 転記した行だけD列を「出荷済」にする(他の行のD列には触れない)
  受注.getRangeList(対象.map(t => 'D' + t.行)).setValue('出荷済');

  ui.alert(`${件数}件を出荷シートに転記しました`);
}

function 基準日() {
  return new Date(2026, 10, 10); // DateSerial(2026, 11, 10)(JavaScript の月は 0 始まり)
}

// 指定列の最後の空でない行番号(列が空なら 1。End(xlUp) と同じ)
function 列の最終行_(sheet, col) {
  const n = sheet.getLastRow();
  if (n === 0) return 1;
  const vals = sheet.getRange(1, col, n, 1).getValues();
  for (let r = n; r >= 1; r--) {
    if (vals[r - 1][0] !== '') return r;
  }
  return 1;
}

// 指定列の空でない値の一覧
function 列の値_(sheet, col) {
  const n = sheet.getLastRow();
  if (n === 0) return [];
  return sheet.getRange(1, col, n, 1).getValues()
    .map(r => r[0])
    .filter(v => v !== '');
}

// Excel の Find(完全一致・大文字小文字を区別しない・全角半角を区別しない)に近い比較用キー
// REVIEW: Excel の照合規則の近似です
function 検索キー_(v) {
  return String(v).normalize('NFKC').toLowerCase();
}

// Excel のボタンの代わりにメニューから実行できるようにする
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('出荷')
    .addItem('出荷転記', '出荷転記')
    .addToUi();
}

確認した結果:出荷待ち 2 件(1001、1003)が出荷シートの 2・3 行目に転記され、転記日は 2026/11/10、受注シートの状態は「出荷済」に変わり、「保留」の行はそのまま。出荷シートに 1003 と 10030 がすでにある状態で実行すると、1001 だけが転記されます(1件)。

実例3:AI チャットや手書きでつまずく「静かな違い」

一番やっかいなのは、エラーにならずに結果だけが変わる違いです。次のマクロは、社内ツールでよく使う計算を並べたものです。

' 社内ツールでよく使う変換・計算のチェック用マクロ
Sub 計算チェック()
    Dim s As String: s = "10"
    Dim r As String

    r = r & "加算=" & (s + 5) & vbLf
    r = r & "連結=" & s & 5 & vbLf
    r = r & "丸め1=" & Round(2.5) & vbLf
    r = r & "丸め2=" & Round(3.5) & vbLf
    r = r & "切捨=" & Int(-2.5) & "/" & Fix(-2.5) & vbLf
    r = r & "剰余=" & (-7 Mod 3) & "/" & (12.6 Mod 5) & vbLf
    r = r & "整数化=" & CInt(2.5) & vbLf
    r = r & "割算=" & 7 / 2 & "/" & 7 \ 2 & vbLf
    r = r & "小数=" & 0.1 + 0.2 & vbLf
    r = r & "数値化=" & Val("12abc") & "/" & CLng("1,234") & vbLf
    r = r & "書式=" & Format(1234567, "#,##0") & "/" & Format(0.256, "0.0%") & "/" & Format(5, "000") & vbLf
    r = r & "日付1=" & Format(DateSerial(2026, 2, 30), "yyyy/mm/dd") & vbLf
    r = r & "日付2=" & Format(DateAdd("m", 1, DateSerial(2026, 1, 31)), "yyyy/mm/dd") & vbLf
    r = r & "月末=" & Format(DateSerial(2026, 3, 0), "yyyy/mm/dd") & vbLf
    r = r & "曜日=" & Weekday(DateSerial(2026, 11, 9)) & vbLf
    r = r & "月差=" & DateDiff("m", DateSerial(2026, 1, 31), DateSerial(2026, 2, 1)) & vbLf
    r = r & "全角=" & StrConv("アイウ", vbWide) & vbLf
    r = r & "文字=" & Len("日本語") & "/" & Mid("請求書番号A-001", 6, 5) & vbLf
    r = r & "検索=" & InStr("abcabc", "c") & "/" & InStr(4, "abcabc", "c") & "/" & InStr("abc", "z") & vbLf
    r = r & "分割=" & UBound(Split("a,b,,c", ",")) & vbLf
    r = r & "比較=" & ("abc" = "ABC") & vbLf
    r = r & "トリム=[" & Trim(" 全角 ") & "]" & vbLf

    MsgBox r, vbInformation, "計算チェック"
End Sub

VBA での結果と、JavaScript でそのまま書いた場合の結果を比べます(JavaScript 側は実際に実行して確認しました)。

項目 VBA の結果 そのまま JavaScript にすると 結果
"10" + 5 15(数値として足す) "10" + 5 "105"(文字列連結)
Round(2.5) / Round(3.5) 2 / 4(銀行型の丸め) Math.round 3 / 4
CInt(2.5) 2 Math.round(2.5) 3
12.6 Mod 5 3(先に整数に丸める) 12.6 % 5 2.5999999999999996
0.1 + 0.2 の表示 0.3 0.1 + 0.2 0.30000000000000004
CLng("1,234") 1234 Number("1,234") NaN
1/31 の 1 か月後 2026/02/28 setMonth(+1) 2026/03/03
Weekday(2026/11/9)(月曜) 2(日曜=1) getDay() 1(日曜=0)
Trim(" 全角 ") 全角スペースは残る trim() 全角スペースも消える
Mid("請求書番号A-001", 6, 5) A-001(1 始まり) substr(6, 5) -001(0 始まり)

請求額の丸めや締め日の計算でこれが起きると、エラーも出ないまま金額や日付がずれます。AI チャットで変換した場合も、こうした点は必ず元の Excel と同じデータで確認してください。

変換後の Apps Script(VBA と同じ結果になるよう補助関数を使っています)

// Module1.gs
// 社内ツールでよく使う変換・計算のチェック用マクロ
function 計算チェック() {
  const s = '10';
  let r = '';

  r += '加算=' + vbStr_(Number(s) + 5) + '\n'; // VBA: 文字列 "10" + 5 は数値の加算になる
  r += '連結=' + s + vbStr_(5) + '\n';
  r += '丸め1=' + vbStr_(vbRound_(2.5)) + '\n'; // VBA の Round は銀行型丸め
  r += '丸め2=' + vbStr_(vbRound_(3.5)) + '\n';
  r += '切捨=' + vbStr_(Math.floor(-2.5)) + '/' + vbStr_(Math.trunc(-2.5)) + '\n'; // Int / Fix
  r += '剰余=' + vbStr_(vbMod_(-7, 3)) + '/' + vbStr_(vbMod_(12.6, 5)) + '\n';
  r += '整数化=' + vbStr_(vbCInt_(2.5)) + '\n';
  r += '割算=' + vbStr_(7 / 2) + '/' + vbStr_(vbIntDiv_(7, 2)) + '\n';
  r += '小数=' + vbStr_(0.1 + 0.2) + '\n';
  r += '数値化=' + vbStr_(vbVal_('12abc')) + '/' + vbStr_(vbCLng_('1,234')) + '\n';
  r += '書式=' + fmtThousands_(1234567) + '/' + fmtPercent1_(0.256) + '/' + fmtZeroPad_(5, 3) + '\n';
  r += '日付1=' + fmtYmd_(vbDateSerial_(2026, 2, 30)) + '\n';
  r += '日付2=' + fmtYmd_(vbDateAddMonth_(1, vbDateSerial_(2026, 1, 31))) + '\n';
  r += '月末=' + fmtYmd_(vbDateSerial_(2026, 3, 0)) + '\n';
  r += '曜日=' + vbStr_(vbWeekday_(vbDateSerial_(2026, 11, 9))) + '\n';
  r += '月差=' + vbStr_(vbDateDiffMonth_(vbDateSerial_(2026, 1, 31), vbDateSerial_(2026, 2, 1))) + '\n';
  r += '全角=' + vbStrConvWide_('アイウ') + '\n';
  r += '文字=' + vbStr_('日本語'.length) + '/' + vbMid_('請求書番号A-001', 6, 5) + '\n';
  r += '検索=' + vbStr_(vbInStr_(1, 'abcabc', 'c')) + '/' + vbStr_(vbInStr_(4, 'abcabc', 'c')) + '/' + vbStr_(vbInStr_(1, 'abc', 'z')) + '\n';
  r += '分割=' + vbStr_('a,b,,c'.split(',').length - 1) + '\n'; // UBound(Split(...))
  r += '比較=' + vbStr_('abc' === 'ABC') + '\n'; // VBA の既定(Binary)比較は大文字小文字を区別
  r += 'トリム=[' + vbTrim_(' 全角 ') + ']' + '\n'; // VBA の Trim は半角スペースのみ削除

  const ui = SpreadsheetApp.getUi();
  ui.alert('計算チェック', r, ui.ButtonSet.OK);
}

// ===== VBA と同じ結果を出すための補助関数 =====

// VBA の文字列変換(数値は有効桁 15 桁、Boolean は True/False)
function vbStr_(v) {
  if (typeof v === 'boolean') return v ? 'True' : 'False';
  if (typeof v === 'number') {
    if (Number.isInteger(v)) return String(v);
    return String(Number(v.toPrecision(15)));
  }
  return String(v);
}

// VBA の Round(x)(銀行型丸め:.5 は偶数側へ)
function vbRound_(x) {
  const f = Math.floor(x);
  const diff = x - f;
  if (diff > 0.5) return f + 1;
  if (diff < 0.5) return f;
  return f % 2 === 0 ? f : f + 1;
}

// VBA の CInt
function vbCInt_(x) {
  const v = vbRound_(Number(x));
  if (isNaN(v) || v < -32768 || v > 32767) throw new Error('オーバーフローしました。');
  return v;
}

// VBA の CLng(桁区切りのカンマを許可)
function vbCLng_(x) {
  const t = String(x).replace(/,/g, '').trim();
  const n = Number(t);
  if (t === '' || isNaN(n)) throw new Error('型が一致しません。');
  const v = vbRound_(n);
  if (v < -2147483648 || v > 2147483647) throw new Error('オーバーフローしました。');
  return v;
}

// VBA の Mod(両辺を先に整数に丸め、符号は左辺に従う)
function vbMod_(a, b) {
  const x = vbRound_(a);
  const y = vbRound_(b);
  if (y === 0) throw new Error('0 で除算しました。');
  const m = x % y;
  return m === 0 ? 0 : m;
}

// VBA の \ 演算子(両辺を整数に丸めてから切り捨て除算)
function vbIntDiv_(a, b) {
  const x = vbRound_(a);
  const y = vbRound_(b);
  if (y === 0) throw new Error('0 で除算しました。');
  const q = Math.trunc(x / y);
  return q === 0 ? 0 : q;
}

// VBA の Val(先頭の数値部分だけを読む。空白は無視)
function vbVal_(text) {
  const t = String(text).replace(/[ \t\r\n]/g, '');
  const m = t.match(/^[+-]?(\d+\.?\d*|\.\d+)([eEdD][+-]?\d+)?/);
  return m ? Number(m[0].replace(/[dD]/, 'e')) : 0;
}

// Format(n, "#,##0")
function fmtThousands_(n) {
  const v = Math.round(Math.abs(n));
  const t = String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return (n < 0 && v !== 0 ? '-' : '') + t;
}

// Format(n, "0.0%")
function fmtPercent1_(n) {
  const v = n * 100;
  return (v < 0 ? '-' : '') + Math.abs(v).toFixed(1) + '%';
}

// Format(n, "000") など(ゼロ埋め)
function fmtZeroPad_(n, width) {
  const v = Math.round(Math.abs(n));
  return (n < 0 && v !== 0 ? '-' : '') + String(v).padStart(width, '0');
}

// VBA の DateSerial(範囲外の月・日は自動で繰り越し)
function vbDateSerial_(y, m, d) {
  return new Date(y, m - 1, d);
}

// Format(d, "yyyy/mm/dd")
function fmtYmd_(d) {
  const y = String(d.getFullYear()).padStart(4, '0');
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}/${m}/${day}`;
}

// DateAdd("m", n, d)(移動先の月に日がなければ月末日に合わせる)
function vbDateAddMonth_(n, d) {
  const y = d.getFullYear();
  const m = d.getMonth() + n;
  const lastDay = new Date(y, m + 1, 0).getDate();
  return new Date(y, m, Math.min(d.getDate(), lastDay), d.getHours(), d.getMinutes(), d.getSeconds());
}

// Weekday(d)(日曜=1 ~ 土曜=7)
function vbWeekday_(d) {
  return d.getDay() + 1;
}

// DateDiff("m", d1, d2)(月の境目をまたいだ回数)
function vbDateDiffMonth_(d1, d2) {
  return (d2.getFullYear() - d1.getFullYear()) * 12 + (d2.getMonth() - d1.getMonth());
}

// StrConv(s, vbWide)(半角英数記号・半角カナを全角に変換)
function vbStrConvWide_(s) {
  const kana = '。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゛゜';
  const dakuOK = 'カキクケコサシスセソタチツテトハヒフヘホ';
  const handakuOK = 'ハヒフヘホ';
  const str = String(s);
  let out = '';
  for (let i = 0; i < str.length; i++) {
    const code = str.charCodeAt(i);
    if (code >= 0xFF61 && code <= 0xFF9F) {
      const w = kana.charAt(code - 0xFF61);
      const next = str.charAt(i + 1);
      if (next === '゙' && dakuOK.indexOf(w) >= 0) {
        out += String.fromCharCode(w.charCodeAt(0) + 1);
        i++;
      } else if (next === '゙' && w === 'ウ') {
        out += 'ヴ';
        i++;
      } else if (next === '゚' && handakuOK.indexOf(w) >= 0) {
        out += String.fromCharCode(w.charCodeAt(0) + 2);
        i++;
      } else {
        out += w;
      }
    } else if (code === 0x20) {
      out += '\u3000';
    } else if (code >= 0x21 && code <= 0x7E) {
      out += String.fromCharCode(code + 0xFEE0);
    } else {
      out += str.charAt(i);
    }
  }
  return out;
}

// VBA の Mid(開始位置は 1 から)
function vbMid_(s, start, len) {
  const str = String(s);
  return str.substring(start - 1, start - 1 + len);
}

// VBA の InStr(見つかれば 1 から数えた位置、なければ 0)
function vbInStr_(start, s, find) {
  if (find === '') return start;
  return String(s).indexOf(find, start - 1) + 1;
}

// VBA の Trim(半角スペースのみ削除。全角スペースは残る)
function vbTrim_(s) {
  return String(s).replace(/^ +| +$/g, '');
}

確認した結果:マクロが出力する 22 項目すべてが VBA と同じ結果になりました(加算=15、丸め1=2、剰余=-1/3、日付2=2026/02/28、曜日=2、トリム=[ 全角 ] など)。

どの方法で移すか

方法 費用の目安 向いているケース
自分で書き換える 自分の時間(数時間〜数日) 数十行まで、JavaScript に慣れている
AI チャット(ChatGPT など) 無料〜 短いコード。上の「静かな違い」は自分で確認が必要
外注 ココナラで 1 本 5,000〜10,000 円程度の出品が多数(検索結果)、制作会社は 2 万円〜(例) 大規模・ユーザーフォームが多い
公式 Macro Converter Enterprise Plus / Education Plus のみ 対象プランを契約済みの組織
VBA Migrate(当サイトのアドオン) 診断は無料、150 行までの 1 本目は無料、以降 1 回 $12(約 1,800 円) 700 行までの一般的な業務マクロ

無料で互換性を診断する

VBA Migrate は、この記事の 3 本を含む 23 本のテストマクロを、実際に実行してセルの値で確認している変換アドオンです。.xlsm を入れるだけで、「そのまま移せる/確認が必要/移せない」処理を一覧にした互換性レポートを無料で出します。変換したコードが動かず、こちらで直せない場合は 14 日以内なら全額返金します。

無料で互換性を診断する →

VBA Migrate は Google および Microsoft とは関係ありません。Google スプレッドシート™・Google Apps Script™ は Google LLC の商標、Microsoft®・Excel® は Microsoft Corporation の商標です。記事内の情報は 2026年11月10日時点のものです。

無料でマクロを診断する