質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

配列

配列は、各データの要素(値または変数)が連続的に並べられたデータ構造です。各配列は添え字(INDEX)で識別されています。

Q&A

解決済

1回答

2433閲覧

【GAS】二次元配列のデータ取得方法とループ処理

donguriko

総合スコア30

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

配列

配列は、各データの要素(値または変数)が連続的に並べられたデータ構造です。各配列は添え字(INDEX)で識別されています。

0グッド

0クリップ

投稿2022/03/20 05:25

編集2022/03/26 06:21

前提・実現したいこと

スプレッドシート内に「入力シート」「マージシート」の2種があります。
「入力シート」は、「部署」~「転記処理」までの6列分×MAX10行で
1ブロック。

GASで以下の2つの処理をしたいです。
①「入力シート」の内容を「マージシート」に転記
②上記①の処理が完了したら、「入力シート」に「転記済」を追記

①の処理の際、二次元配列でデータ取得したいです。
8行目スタートで、C列~G列までの1行分をまとめて取得。
ループ処理で1行ずつ下に移動させ、取得させる想定です。

添付イメージだと、
ループ処理はIndex0からIndex4まで計5回処理の想定。

<入力シートイメージ>
イメージ説明

<マージシートイメージ>
イメージ説明

発生している問題・エラーメッセージ

下のエラーが出てしまいます。
1つ目の8行目のデータ取得はできているのですが、
2つ目の9行目のデータ取得が失敗しています。

TypeError: Cannot read property '0' of undefined copyToMerge1 @ 入力シート→マージシートへの転記ver1.1.gs:24

該当のソースコード

以下、コード全文記載します。

GAS

1function copyToMerge1() { 2 const ss = SpreadsheetApp.getActiveSpreadsheet(); 3 const mySheet1 = ss.getSheetByName("入力シート"); 4 const mySheet2 = ss.getSheetByName("【●●】マージ"); 5 const mySheet3 = ss.getSheetByName("【■■】マージ"); 6 7 //【ブロック内のループ】 8 //件数データありの最終行(InMaxRow)、転記処理対象の行数(targetRows)の取得 9 const InMaxRow = mySheet1.getRange(18, 4).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); 10 const MaxIndex = InMaxRow - 8; 11 console.log("MaxIndex " + MaxIndex); //Index最大値は4の想定 12 13 //「部署」~「所要時間」までの5列分をgetRange(行、列、▲行分、■列分) で 14 // 二次元配列(Values)として取得 15 // ループ処理は0スタートでrがMaxIndex以下になるまで繰返し。 16 for (r = 0; r <= MaxIndex; r++) { 17 const myDetaRange = mySheet1.getRange(8 +r, 3, 1, 5); 18 console.log("myDetaRange " + myDetaRange.getA1Notation()); 19 const values = myDetaRange.getValues(); 20 21 //二次元配列(Values)内の項目は「部署」「件数」「開始時刻」「終了時刻」「所用時間」の5項目 22 let Branch = values[r][0]; 23 let Num = values[r][1]; 24 let startTime = values[r][2]; 25 let endTime = values[r][3]; 26 let length = values[r][4]; 27 console.log("values " + values); 28 29 //【マージシートへの転記処理】 30 //▼「×××マージ」シート(mySheet2)の件数(E列)にデータありの最終行(OutMaxRow1)の取得 31 const OutMaxRow1 = mySheet2.getRange(mySheet2.getMaxRows(), 5).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); 32 console.log("OutMaxRow1 " + OutMaxRow1); //10が出る想定 33 34 //▼作業日(入力シートセルE3)の内容を「マージシート(mySheet2)」のB列に転記 35 const myDate = mySheet1.getRange("E2").getValue(); 36 console.log("myDate " + myDate); 37 mySheet2.getRange(OutMaxRow1 + 1 + r, 2, 1, 1).setValue(myDate); 1 38 39 //▼二次元配列(Values)の内容(5項目)を「マージシート(mySheet2)」のD列~G列に転記 40 //getRange(行、列、▲行分、■列分) 41 mySheet2.getRange(OutMaxRow1 + 1 + r, 4, 1, 5).setValues(values); 42 43 //▼マージシートへの転記結果を「入力シート(mySheet1)」の「転記処理」欄に追記する 44 //getRange(行、列、▲行分、■列分) 45 mySheet1.getRange(8 + r, 3 + 5, 1, 1).setValue("転記済"); 46 } 47 48 //▼ダイアログMsgの表示 49 Browser.msgBox("「マージシート」への転記処理が完了しました。\n「転記処理」列に「転記済」表示があれば問題なく転記処理がされています。", Browser.Buttons.OK); 50} 51 52

試したこと

ログを確認すると、2回目のループ処理のデータ取得範囲myDetaRangeは、
myDetaRange C9:G9 となっていますが、
そこで処理が止まり、以降の配列データの取得ができていないようです。

以前に別件照会した、同エラーが出た際の対処→別件照会①も参照し、
forループの範囲指定を確認しましたが、今回のケースではIndex最大値の[4]
を超えていないのではないかと思っています。

<<教えてほしいこと>>
前回ご回報いただいた別件照会①の2/2の「さら問い」の配列の作り方と
ループ処理の関係が理解しきれていないようです。→別件照会①
現コードのどこに誤りがあるのか、どのように修正すればいいのかが
わかりません。

配列(Values)のデータ取得範囲myDetaRangeの指定の際、
第3引数? を1行分としている点が誤りですか?
1行ずつループ処理させたい場合は、r行分まとめて範囲指定
が正しいですか??

もし、データ取得範囲myDetaRangeを1行とする場合なら、
都度配列(Values)に取得したデータを投入していかないと
配列(Values)内のIndexが0、1、2、、、とならない、
ということですか?

何度もご教示いただいているのに習得しきれておらず本当に申し訳ありません。
次こそは、自力解決できるようになりたいので、お手数をおかけして
大変申し訳ありませんが、回答は急ぎませんのでビギナーでも理解できる
レベルでの解説をいただけないでしょうか?

補足情報(FW/ツールのバージョンなど)

本件、別照会で1度照会しています。→別件照会②

今回のコードでは、ループはブロック内のループのみ記載ですが、
前回照会ではブロック間とブロック内の2つのループをさせる想定の
コードでした。
前回回答内容をもとにコードの自力修正を試みましたが、私の知識が
足りず自力解決できませんでした。

そのため、コードを分解し、まずはブロック内で正しく処理が走るよう
1つずつ修正を試みようと思っている次第です。
今回の問題が解決できたら、次ステップとして、
ブロック②、③、、、とブロック間で連続して処理が走るように
拡張させていきたいと思っています。

(参考) 回答を受けて修正したコード(追記)

回答を受け、コードを以下のとおり修正しましたので参考掲載します。
無事処理が走りました。
アドバイスありがとうございました。大変助かりました。

GAS

1function copyToMerge1() { 2 const ss = SpreadsheetApp.getActiveSpreadsheet(); 3 const mySheet1 = ss.getSheetByName("キイロ入力シート"); 4 const mySheet2 = ss.getSheetByName("【●●】マージ"); 5 const mySheet3 = ss.getSheetByName("【■■】マージ"); 6 7//★ブロック間のループ 8//「部署」~「所要時間」までの6列×転記処理対象最終行(MaxRows)分 9//で1ブロックとする。 10//ブロックの開始col(blockCol)はC列(3col)とし、6colとばしで7ブロック目(39col目)まで 11 for (blockCol = 3; blockCol <= 6*6 +3; blockCol +=6) { 12 13 //★ブロック内のデータ取得 14 //件数データありの最終行(InMaxRow)、転記処理対象最終行(MaxRows)の取得 15 const InMaxRow = mySheet1.getRange(18, blockCol+1).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); 16 const MaxRows = InMaxRow - 7; 17 console.log("MaxRows " + MaxRows); //5の想定 18 19 //「部署」~「所要時間」までの5列分×MaxRows行分をgetRange(行、列、▲行分、■列分) で 20 // 二次元配列(Values)として1回でまとめて取得 21 const myDetaRange = mySheet1.getRange(8, blockCol, MaxRows, 5); 22 console.log("myDetaRange " + myDetaRange.getA1Notation()); 23 const values = myDetaRange.getValues(); 24 console.log("values " + values); 25 26 //★マージシートへの転記処理 27 //▼「×××マージ」シート(mySheet2)の件数(E列)にデータありの最終行(OutMaxRow1)の取得 28 const OutMaxRow1 = mySheet2.getRange(mySheet2.getMaxRows(), 5).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); 29 console.log("OutMaxRow1 " + OutMaxRow1); //10が出る想定 30 31 //▼作業日(入力シートセルE3)の内容を「マージシート(mySheet2)」のB列に転記 32 const myDate = mySheet1.getRange("E2").getValue(); 33 console.log("myDate " + myDate); 34 mySheet2.getRange(OutMaxRow1+1, 2, MaxRows, 1).setValue(myDate); 1 35 36 //▼二次元配列(Values)の内容(5項目)を「マージシート(mySheet2)」のD列~G列に転記 37 //getRange(行、列、▲行分、■列分) 38 mySheet2.getRange(OutMaxRow1 +1, 4, MaxRows, 5).setValues(values); 39 40 //▼マージシートへの転記結果を「入力シート(mySheet1)」の「転記処理」欄に追記する 41 //getRange(行、列、▲行分、■列分) 42 mySheet1.getRange(8, blockCol+5, MaxRows, 1).setValue("転記済"); 43 } 44 45 //▼ダイアログMsgの表示 46 Browser.msgBox("「●●マージシート」への転記処理が完了しました。\\n入力シートの「転記処理」列に「転記済」表示があれば問題なく転記処理がされています。", Browser.Buttons.OK); 47} 48 49 50 51

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

・エラーの原因

19行目~21行目あたり

js

1 const myDetaRange = mySheet1.getRange(8 +r, 3, 1, 5); 2 console.log("myDetaRange " + myDetaRange.getA1Notation()); 3 const values = myDetaRange.getValues();

上の引用部分の1行目で、myDetaRange は、5列1行のデータになっています。
(getRange()の第3引数が「1」であるため)

myDetaRange は1行しか指定されていないため、
const values = myDetaRange.getValues();
とすると、valuesは values[0][] までしかデータが入っていないことになります。


その後(23行目以降で)

js

1 //二次元配列(Values)内の項目は「部署」「件数」「開始時刻」「終了時刻」「所用時間」の5項目 2 let Branch = values[r][0]; 3 let Num = values[r][1]; 4 let startTime = values[r][2]; 5 let endTime = values[r][3]; 6 let length = values[r][4];

としていますが、上述の通り、valuesはループ毎に毎回1行分しかデータが入っていない(= values[0][] までしかデータがない)のですから、
forループが1回まわって、r が 1になったとき
let Branch = values[1][0];
を実行したところで、存在しないインデックス(1)にアクセスすることになり、エラーが発生しています。


・修正案

ループごとにvaluesは1行分ずつしか取得していないのですから、
下記のように、values[0][~] とします。

js

1 //二次元配列(Values)内の項目は「部署」「件数」「開始時刻」「終了時刻」「所用時間」の5項目 2 let Branch = values[0][0]; // [r] -> [0] に修正。以下4行同じ 3 let Num = values[0][1]; 4 let startTime = values[0][2]; 5 let endTime = values[0][3]; 6 let length = values[0][4];

 
また、33行目以降で、 ループごとにマージシートの最終行(OutMaxRow1)を取得しているにもかかわらず
ループカウンタ r を用いてさらに行を加算しているので
このままだとループが回る度にマージシートの転記先の前に空白行ができてしまいます。
したがって、下記のように、マージシートに転記する部分では、r の加算は不要です。

(31行目以降)

diff

1 //【マージシートへの転記処理】 2 //▼「×××マージ」シート(mySheet2)の件数(E列)にデータありの最終行(OutMaxRow1)の取得 3 const OutMaxRow1 = mySheet2.getRange(mySheet2.getMaxRows(), 5).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); 4 console.log("OutMaxRow1 " + OutMaxRow1); //10が出る想定 5 6 //▼作業日(入力シートセルE3)の内容を「マージシート(mySheet2)」のB列に転記 7 const myDate = mySheet1.getRange("E2").getValue(); 8 console.log("myDate " + myDate); 9- mySheet2.getRange(OutMaxRow1 + 1 + r, 2, 1, 1).setValue(myDate); 1 10+ mySheet2.getRange(OutMaxRow1 + 1 , 2, 1, 1).setValue(myDate); 1 11 12 //▼二次元配列(Values)の内容(5項目)を「マージシート(mySheet2)」のD列~G列に転記 13 //getRange(行、列、▲行分、■列分) 14- mySheet2.getRange(OutMaxRow1 + 1 + r, 4, 1, 5).setValues(values); 15+ mySheet2.getRange(OutMaxRow1 + 1 , 4, 1, 5).setValues(values);

コメントの質問に対する回答

<さら問い1>

23行目以降でValues[0][~]となっているので、
ループ2回目がまわる時、配列Valuesの中にはループ1回目の
データが残っている状態かと思います。
データが残っていても2回目のループ処理で、1回目ループの
データは上書きされるので残っていても問題なし、という
理解であっていますか?

→ 厳密には、valuesはローカル変数であり、(少なくとも理論的には)ループの度に生成されています。
2回目のループにおけるvaluesは、1回目のループのvaluesとは、名前が一緒ですが別物です。
したがって2回目のループで、"valuesの中に1回目のデータが残っているが上書きされる"ということではありません。
「前と違う、新しいvaluesという名前に、次の行の配列データが結び付けられる」と言った方が適切かもしれません。
いずれにしても1回前に取得したデータは影響しないため、問題はないという結論に変わりはありません。

<さら問い2>

今回ブロック内の処理をする際、
a)5列1行ずつデータを取得し、配列を作る方法
b)複数行分をまとめて一度で配列を作る方法
どちらの方法がおススメでしょうか? 

b) の方がおすすめですね。
理由:少しでも処理を速くするためです。
getRange / getValue[s] / setValue[s] のようなAPIの実行には時間がかかります。
(ここでの「API」とは、SpreadsheetApp. ~ で呼び出す関数全般と考えてもらってよいです)
一方、一度配列として読み込んだデータを加工する処理(push等を含む)は、APIの実行に比べ、たいてい格段に短く済みます。

getValuesを使うときは、なるべく広い範囲をまとめて配列として読み込む

配列データ加工(pushやmap等で新しい配列を作る等)

まとめてsetVauesで書き込み

というようにして、API呼び出し回数をなるべく減らします。

たとえば100行のデータを1行1行getValueで読み込んで、加工し、1行ずつsetValueで書き込むと、最大200回API呼び出しすることになりますが、
100行まとめてgetValuesで読み込み、配列データとして加工(pushやmap等で配列を作る処理含む)し、まとめてsetVauesで書き込めば、API呼び出しは2回で済みます。

投稿2022/03/20 06:08

編集2022/03/20 22:22
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

donguriko

2022/03/20 08:47 編集

qnoirさま お忙しい中、早々のご回答本当にありがとうございます。 myDetaRangeとして5列1行ずつデータ取得していく場合は、 ループ1回目もループ2回目も配列Valuesは常にデータは1行分 しかないので、行インデックスは常に[0]なのですね。 <さら問い1> 素人質問で申し訳ありません。 23行目以降でValues[0][~]となっているので、 ループ2回目がまわる時、配列Valuesの中にはループ1回目の データが残っている状態かと思います。 データが残っていても2回目のループ処理で、1回目ループの データは上書きされるので残っていても問題なし、という 理解であっていますか? <さら問い2> 別解として、5列1行ずつでループをまわすのではなく、 開始行8行目固定とし、複数行分をまとめて取得し、一度に 5列×最終行までの複数行分の二次元配列を作る方法も 可能かと思います。 今回ブロック内の処理をする際、 a)5列1行ずつデータを取得し、配列を作る方法 b)複数行分をまとめて一度で配列を作る方法 どちらの方法がおススメでしょうか?  もし、qnoirさまでしたらどちらにされますか? また、どうしてなのかについても考え方のアドバイスを いただけないでしょうか? 照会した当初、a)とb)が混乱気味でしたが、回報内容を読込みながら 頭を再整理していたら、もしかするとブロック内はb)とした方が、 実はよいのではないかという気になりました。 どのアプローチがよいのかの考え方の参考とさせていただきたいです。 以前にもアドバイスいただいたように、後々の修正メンテ等を考えると コードは簡潔に済む方がよいので、やはりおススメはb)でしょうか?
退会済みユーザー

退会済みユーザー

2022/03/20 13:45

追加質問に対して回答欄に追記しました。 実は最初質問のコードを見た時にはまとめて複数行配列にした方が効率が良いかもと思ったのですが、 エラーとなっている原因を明らかにする説明を優先したので回答内の修正案は1行1行処理するやり方になっています。 以前に比べると、きちんとconsole.logで内容確認されていたり、コードの方向性もしっかりされている印章で、成長を感じます。まとめて書くやり方については、回答を出すこともできますが、ひとまず御自分で考えられてはいかがでしょうか。どうしてもわからなければもちろんヘルプします。
donguriko

2022/03/20 15:07 編集

qnoirさま お忙しい中、さら問いへの回答ありがとうございます。 配列やループについての理解が追い付かず、何度も何度も質問してしまい その度に本当に心苦しかったのですが、今回のqnoirさまからの優しいコメント、 すごくうれしいです!! ここまで折れずにGAS独学がなんとか続けられているのは、どんなに初歩的な 質問でも毎回丁寧に解説をしてくださるqnoirさまのおかげです。 感謝しかないです。 b) の方がおすすめとのことなので、まずは自力でb)のコードを 考えてみます。 もし、又躓いたり、アドバイスをいただきたい時にはご相談させてください。 エラーの原因解消のレクチャーのため、a)方式におつきあいいただき感謝です。 おかげさまでa)とb) 両方のコードの書き方の勉強ができます。 いつも、親身になってアドバイスをくださり、本当にありがとうございます。 <相談> もし、自力で書いたコードでなんとか想定どおりの処理が走った場合、 同じことで悩んでいる方のために、本質問に自力記載したコードを 追記しておいた方がよいでしょうか? 追記した方がよい場合は、記載する欄は下の「解決方法」欄で よいですか?
退会済みユーザー

退会済みユーザー

2022/03/20 15:38

後から見た人の参考になると思うので、追記することをお勧めします。 記載する欄は、質問欄の続きがよいかと思います(コメント欄だとソースコードをフォーマットして貼り付けることができないため)。 記載するか否か、どこに記載するかは最終的にはお任せします。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問