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

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

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

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

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

2回答

1527閲覧

gasでスプレッドシート内の表データを加工して貼り付けたい。

hamada1110

総合スコア0

Google Apps Script

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

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

0クリップ

投稿2020/11/10 08:50

前提・実現したいこと

gasでスプレッドシート内の表データを加工して貼り付けたい。

画像1
イメージ説明

画像2
イメージ説明

画像1のような請求書作成のためのデータを格納した表があり、
これらをgasで整理し、同じ顧客への請求をまとめたいと思います。

例えば画像1内の顧客名「いいい」は3行目「訪問料」と4行目「顧問料」が存在していますので
画像2の3行目のように顧問料列、訪問料列にそれぞれ入力しようとしています。

しかし、同じ顧客であれば全て1行にするのではなく事業所Aと事業所Bの請求は別の請求書を発行する
必要があるため、加工する際も分ける必要があります。
(→画像2でいう顧客名「あああ」のような感じです。)

このように事業所ごとに請求をまとめて別のスプレッドシートへ出力したいと考えています。

発生している問題

スプレッドシートで大量のデータを扱う場合、配列というものを使ったほうがよいとのことですが
配列の加工方法を探してみても「配列内のデータを削除」や「配列に値をを加える」といった
ものばかりで、なかなか自分の探しているものがみつかりません。

配列内で加工をしようとしているからダメなわけで
配列を2つ用意して、配列1に加工前のデータを格納して配列2に加工したものをfor文で
入れていけばいいのかな?なども考えていますが、何分最近触り始めたばかりですので
これが良い処理方法なのか悪い処理方法なのかも判断ができないのです。

皆様はどのように処理を行うのでしょうか?
ご教授いただければ幸いです。

現状できているところまでのソースコードもはりますので
「こんな処理は避けたほうがいい」というものがありましたら
お願いいたします

gas

1var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); 2var sheet = spreadsheet.getActiveSheet(); 3var date = new Date(); 4var testsheet = spreadsheet.getSheetByName("test"); 5 6 7function NarrowDown_CheckList() { 8 9 var lastRow = sheet.getRange(2, 3).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow(); //getRange([行番号], [列番号]) 10 var lastCol = sheet.getRange(2, 1).getNextDataCell(SpreadsheetApp.Direction.NEXT).getColumn(); 11 var month = date.getMonth() + 1; //今月を取得 12 var firstRow = 3; //項目行を無視(データが格納されている最初の行を指定) 13 14 15 16 const dataRange = sheet.getRange(firstRow, 1, lastRow, lastCol); 17 const values = dataRange.getValues(); 18 19const result = values.filter(rowData => rowData[month+6] == "1"); //今月請求する項目のみを抽出([month+6]が当月の列) 20var Vertical = result.length; //配列タテ 21var side = result[0].length; //配列ヨコ 22 23 24 25 26 27testsheet.getRange(2,1,Vertical,side).setValues(result) //テスト出力用 28}

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

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

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

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

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

Automatic9045

2020/11/10 09:08

配列がどのような概念かは理解されていますか? 2次元配列の使用経験はありますか?
hamada1110

2020/11/10 10:36

配列に関してはだいたい理解できていると思っています。 私のイメージ的には 配列はcsvみたいなもの。1行のみのcsvが一次元配列、複数行のcsvが2次元配列で csvで用意したデータを別のソフトが読み取れるように配列に入っている値は 好きなタイミングで好きな場所を読み取れるデータ郡 といったイメージを持っています。 しかし経験となると・・・ 1週間ほど前にプログラミングを初めて触りだしたので 2次元配列含めて経験はありません。 よろしくお願いいたします
Automatic9045

2020/11/10 11:06

なるほど、csvみたいなものですか。良い考え方だと思います。 回答を書くので少々お待ちを。
sawa

2020/11/11 00:12

ピボットテーブル機能やQuery関数でも希望に近い表は出力できそうですが、GASでやりたいってことでしょうか?
hamada1110

2020/11/13 00:23

>sawa様 ありがとうございます。 「前提・実現したいこと」では簡単に整理しておりましたが実際の運用にあたっては 6法人7事業所で毎月800件ほど請求が発生し、かつ毎月全体の3%ほどの請求が前月の請求内容から変更されます。 過去の請求の確認が必要になった際に請求履歴などを確認できるよう、gasでトランザクションシートに蓄積・Query関数で取り出しを行う予定です。 よろしくお願いいたします。
guest

回答2

0

javascript

1const q303432 = () => { 2 const sheet1 = "シート1"; 3 const sheet2 = "シート2"; 4 const groupingKey = ["担当事務所", "顧客コード"]; 5 const item = "請求内容"; 6 const monthStarts = 4; 7 const targetFixHeader = ["請求年月日", "担当事務所", "顧客コード", "顧客名"]; 8 const sum = "売上計"; 9 const data = SpreadsheetApp.getActive().getSheetByName(sheet1).getDataRange().getValues(); 10 const source = new src(data,groupingKey); 11 const codeIdx = source.getHeaderIdx(groupingKey[1]); 12 const itemIdx = source.getHeaderIdx(item); 13 const customers = codeToName(data.slice(1),codeIdx,codeIdx + 1); 14 const tgt = new target(targetFixHeader,arrayUnique(arrayColumn(data.slice(1),itemIdx)),sum); 15 source.getKeys().forEach(e => { 16 const assets = source.getRowOf(e); 17 for(let i = monthStarts; i < monthStarts + 12; i++) { 18 const groupedValue = assets[0].getKeys(); 19 let row = [data[0][i],...groupedValue,customers.get(groupedValue[1]),...Array.from({length:tgt.getSumColumnsLength()},_ => 0)]; 20 for(let j = 0; j < assets.length; j++) { 21 row[tgt.getHeaderIdxOf(assets[j].getValue()[itemIdx])] = Number((assets[j].getValue()[i]))*(assets[j].getValue()[itemIdx+1]); 22 } 23 tgt.setRow(row); 24 } 25 }); 26 const results = tgt.toArray(); 27 if(results.length < 1) return; 28 const sheet = SpreadsheetApp.getActive().getSheetByName(sheet2); 29 sheet.getRange(1,1,results.length,results[0].length).setValues(results); 30 sheet.getRange(2,1,sheet.getLastRow(),sheet.getLastColumn()).sort([{column: 1, ascending: true}, {column: 2, ascending: true}]); 31}; 32 33const arrayColumn = (array,idx) => array.map(e=>e[idx]); 34const arrayUnique = (array) => [...(new Set(array))]; 35const codeToName = (data,keyIdx,valIdx) => { 36 const keys = arrayColumn(data,keyIdx); 37 const values = arrayColumn(data,valIdx); 38 const uniqueKeys = arrayUnique(keys); 39 return new Map(uniqueKeys.map(e=> [e, values[keys.indexOf(e)]])); 40}; 41 42 43class src { 44 constructor(array,groupingKey) { 45 this.header = [...array[0]]; 46 this.groupingKeyMap = new Map(groupingKey.map(e => [e, this.header.indexOf(e)]).filter(e => e[1] !== -1)); 47 this.row = array.slice(1).map(e => new srcRow(e, this.groupingKeyMap)); 48 this.groupedKeys = [...(new Set(this.row.map(e => e.getKeyString()))).values()]; 49 this.groupedRows = new Map(this.groupedKeys.map(e => [e, this.row.filter(f => f.isBelongsTo(e))])); 50 } 51 getHeaderIdx(name) { 52 return this.header.indexOf(name) || -1; 53 } 54 getKeys() { 55 return [...this.groupedKeys]; 56 } 57 getRowOf(key) { 58 return this.groupedRows.get(key); 59 } 60} 61 62class srcRow { 63 constructor(array, keyMap) { 64 this.keys = keyMap; 65 this.value = [...array]; 66 this.groupBy = [...this.keys.values()].map(e=>this.value[e]); 67 this.idString = this.groupBy.join("\t"); 68 } 69 getKeys() { 70 return [...this.groupBy]; 71 } 72 getKeyString() { 73 return this.idString; 74 } 75 isBelongsTo(needle) { 76 return needle === this.idString; 77 } 78 getValue() { 79 return [...this.value]; 80 } 81} 82 83class target { 84 constructor(header,sumColumns,sum) { 85 this.header = [...header,...sumColumns,sum]; 86 this.sumColumnsName = [...sumColumns]; 87 this.sumColumns = sumColumns.map(e=>this.header.indexOf(e)); 88 this.rows = []; 89 } 90 getSumColumnsLength() { 91 return this.sumColumnsName.length; 92 } 93 getHeaderIdxOf(name) { 94 return this.header.indexOf(name); 95 } 96 setRow(array) { 97 const sum = this.sumColumns.map(e=>array[e]).reduce((a,c)=>a+c); 98 if (sum === 0) return; 99 this.rows.push([...array,sum]); 100 } 101 toArray() { 102 return [this.header,...this.rows]; 103 } 104}

シート1

flg担当事務所顧客コード顧客名1月2月3月4月5月6月7月8月9月10月11月12月請求内容金額メモ
事業所ACODE01111111111111顧問料100
事業所ACODE01111111111111訪問料150
事業所ACODE01111111111111その他80手数料
事業所ACODE021顧問料1000毎年2月
事業所ACODE02111111111111訪問料500
事業所ACODE03111111111111訪問料1000
事業所ACODE03111111111111その他50
事業所ACODE04111111111111その他80手数料
事業所BCODE02111111111111顧問料600
事業所BCODE02111111111111その他30
事業所BCODE031顧問料300毎年6月
事業所BCODE03111111111111訪問料400
事業所BCODE04111111111111顧問料250
事業所BCODE04111111111111訪問料120
事業所BCODE04111111111111その他10

シート2

請求年月日担当事務所顧客コード顧客名顧問料訪問料その他売上計
2020/01/01事業所ACODE0110015080330
2020/01/01事業所ACODE0205000500
2020/01/01事業所ACODE0301000501050
2020/01/01事業所ACODE04008080
2020/01/01事業所BCODE02600030630
2020/01/01事業所BCODE0304000400
2020/01/01事業所BCODE0425012010380
2020/02/01事業所ACODE0110015080330
2020/02/01事業所ACODE02100050001500
2020/02/01事業所ACODE0301000501050
2020/02/01事業所ACODE04008080
2020/02/01事業所BCODE02600030630
2020/02/01事業所BCODE0304000400
2020/02/01事業所BCODE0425012010380
2020/03/01事業所ACODE0110015080330
2020/03/01事業所ACODE0205000500
2020/03/01事業所ACODE0301000501050
2020/03/01事業所ACODE04008080
2020/03/01事業所BCODE02600030630
2020/03/01事業所BCODE0304000400
2020/03/01事業所BCODE0425012010380
2020/04/01事業所ACODE0110015080330
2020/04/01事業所ACODE0205000500
2020/04/01事業所ACODE0301000501050
2020/04/01事業所ACODE04008080
2020/04/01事業所BCODE02600030630
2020/04/01事業所BCODE0304000400
2020/04/01事業所BCODE0425012010380
2020/05/01事業所ACODE0110015080330
2020/05/01事業所ACODE0205000500
2020/05/01事業所ACODE0301000501050
2020/05/01事業所ACODE04008080
2020/05/01事業所BCODE02600030630
2020/05/01事業所BCODE0304000400
2020/05/01事業所BCODE0425012010380
2020/06/01事業所ACODE0110015080330
2020/06/01事業所ACODE0205000500
2020/06/01事業所ACODE0301000501050
2020/06/01事業所ACODE04008080
2020/06/01事業所BCODE02600030630
2020/06/01事業所BCODE033004000700
2020/06/01事業所BCODE0425012010380

(出力はまだ続きます)

投稿2020/11/12 16:54

papinianus

総合スコア12705

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

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

papinianus

2020/11/12 17:19

この画像2の結果は請求書のフォーマットとしては普通には考えられない体裁で、これをさらに請求書にするため、別な表にすることが予想されます(もしこれが最終形態で、画像2の1行を請求書として送っているというのだとしたら、その体裁をまず見直したほうがいいと思います)。 ・提案(下に行くほど技術寄りです) - 上記の予想も含んでですが、本当にやりたいことやらないといけないことは画像2を作ることなのでしょうか。電子化、自動化は今あることをそのまま機械処理にすることじゃなくて、業務分析してやるべきことにフォーカスしないと、電子化、自動化の工数がかさみます。 - 顧問料の金額がそれぞれ異なることが疑問です。顧客コードもそうですが、一意のコードを振って名前は別管理にしないと、揺れが意図したものか誤記なのか(質問だから適当なのか)分かりません。 - 請求内容(摘要や科目という名前が適切です)がこの3つであることやこの表の体裁がどうなっているのかなど、仕様として確定していないことが多いと感じます。 - 画像1も画像2も会計データとして特殊なかたちなので、1を2にすることは回答の例のとおり原理的には可能です。ただ、画像1が本来の姿に列が変更された場合であっても、うまく画像2に似た姿にするというのはかなり難しいです。なので、仕様の確定が必要です。 - 配列の操作を探して出てこないのは、データ構造が適切でないからです。データ構造が独特で、やることを一般的な方法に抽象化できないので、探しようがないのです。もちろん細分化していけば個々のコードは既知のやりかたの組み合わせに過ぎませんが、1行ずつのレベルまで分解してやりかたを探して、さらにそれらを使って目的の姿に組み上げるのは一般にハードルが高い作業だと感じます。 - データ構造はロジックに影響します。整った姿をしていれば、処理もコードも簡単になります。コードの難度が高いのはデータがおかしな姿だからです。特注品、オーダーメイドがやりたければ相応に高いコストを支払う必要があります。 - 「配列を使う」ことが目的ではないはずなので、知見のある人に依頼して型を正しく使ったコーディングをしたほうがいいです。 -- これかいてて気付きましたが、当月を出すには、"1月" が実際はどのような値("1月"なのか 2020/01/01 なのか)でそれをどう書式設定しているかなどが必要です。 日付でソートしているので、できたやつをフィルタしてくださればいいんじゃないでしょうか。
hamada1110

2020/11/13 01:38

>papinianus 様 回答ありがとうございます。 仰るとおり、データとしてかなり歪な形をしているかと思います。 と、いうのも弊社は税理士、社労士、弁護士など複数の士業法人の集まりです。 「異なる士業同士ですので業務の専門性が180度異なるが、請求の発行部署として日々の業務の消化・請求内容の集計をしなければならない」という課題に直面しております。 税理士用業務ソフト、弁護士用業務ソフトなどそれぞれの士業用の管理ソフトは存在していますが、異なる士業を横断的に使える業務ソフトは存在していません。 今まではこれらを人力でEXCELとスプレッドシートで集計していましたが、限界が見え始めたので自力でなんとかしようとしたのが今回gasを触り始めた要因でした。 -- さて、コードを作成いただきありがとうございます。 こちらのコードと提案いただいた内容を元に弊社に合うように勉強しつつ自力でコードを完成させていきたいと思います。 提案いただいた内容である 「>請求内容(摘要や科目という名前が適切です)がこの3つであることやこの表の~」 →実際には税理士は「税務書類作成報酬」など、弁護士は「法律相談顧問報酬」などといったように それぞれの科目が設定されていますが、グループを横断的に見るために各法人の請求を性格(月々、年1など)で分類し集計しやすくしている。 「>顧問料の金額がそれぞれ異なることが疑問です。~」 →(例えば税理士業務の場合は)年間の仕訳数に応じて顧問料が決定しているため、顧客とグループのどの法人と契約しているかにより顧問料が変動している。 などなどグループの内部処理的に集計しやすいようにデータの流れを統一化することが難しい状況がありまして、オーダーメイドなども考えましたが 一見士業の6法人と大きく見える弊グループですが実態は個人レベルの事務所の集まりでして資金的にも難しい状況です。 以上のことから時間がかかってでも自力でなんとかしないと!との状況でしたがコードをいただけたおかげで かなり作業が進みそうです。 全体の流れも見えてきましたので引き続き自力でなんとかできないか奮闘していみます。 ありがとうございました。
papinianus

2020/11/20 15:31

これがこの 3 つの科目に限定され(つまり勘定科目を集約した結果、必ず 3 つに絞り込まれ)、かつこの表がこの体裁であるのであれば、このコードでシート 1 がシート 2 になります。手直しは不要です。 > 事業所Aと事業所Bの請求は別の請求書を発行する必要があるため、加工する際も分ける必要があります。 このとおり質問では、請求書発行を前提としているように拝見しました。会計に長けておられるかたもいらっしゃるようなのでご承知かと存じますが、普通請求書は1枚の請求書にさらに細目が表としてつきますよね。 画像2 の ABCD 列は請求書に対して必ず 1 個となる項目である一方、EFG(H)は細目として表になる項目、つまり、1請求書に対して、n 個(1-3個)の項目だと考えられます。 一般的な請求書の帳票にしようとしたとき、画像2では一旦横1列にしたものをまた n 倍する作業が必要なので、とても扱いにくい形式だと思います。 ところが目的が > 内部処理的に集計しやすいようにデータの流れを統一化 であれば話はかわってきます。画像2が最終的に欲しい形態なのでしょうから。そしてそれが得られるコードを提供して発言しています。 配列操作が知りたいのであって内容への提言は求めない、ということであれば、配列操作に帰着できるような純粋なコード上の問題に、問題を限定し分割していただければと思います。
guest

0

処理の流れの方針のみ示そうと思います。


GASではスプレッドシート関連の関数を1つ書く度にサーバーと通信する仕様です。この通信がなかなかの遅さなので、基本的に出来るだけ通信する回数が少なくなるように設計します。

例を示しましょう。
1行目から100列目までについて、A列とB列の値を取得したい場合、以下のように書くと、通信する回数は200回になってしまいます。

GAS

1// const sheet = …中略…; 2for (let i = 1; i <= 100; i++) { 3 const valueA = sheet.getRange(i, 1).getValue(); 4 const valueB = sheet.getRange(i, 2).getValue(); 5 Logger.log(valueA + ", " + valueB); 6}

通信する回数を減らす為に良く使うのが、シート内の値をまるまる2次元配列に代入してしまう方法です。この方法を用いると以下のように書け、この場合通信する回数は1回のみです。

GAS

1// const sheet = …中略…; 2const values = sheet.getRange(1, 1, 100, 2); 3values.forEach(row => Logger.log(row[0] + ", " + row[1]));

さて、これを踏まえて本題に入りましょう。

##1. シート全体を取得
先程の例に従い、まずはセオリー通り、シート全体の値を取得して2次元配列(名前はvaluesなどで良いでしょう)に代入しておきます。
内容を含む最後の行・列は、それぞれsheet.getLastRow()、sheet.getLastColumn()で取得できるので、これを用いれば値を取得するべき範囲を動的に指定することが可能です。

##2. 事業所ごとに分割
先程取得した表について、各行を事業者ごとに振り分けていきます。
事業所がAとBの2種類のみであればofficeAValues、officeBValuesなどと事業所数分の変数をつくってやれば大丈夫ですが、事業所が多い場合や今後事業拡大の可能性がある場合は、JSONを用いて1つの変数にまとめると良いかもしれません。
JSONの構造は以下のようにします。

JSON

1{ 2 "事業所A": [ 3 […中略… "いいい", …中略… "顧問料", 1000, "毎年2月請求"], 4 …中略… 5 ], 6 "事業所B": [ 7 …中略… 8 ] 9}

##3. 同一顧客名の行を統合しながら新しいシートに記入する
ここが最も実装の面倒な部分だと思います。
おおまかな流れのイメージは以下の通りです。

新しいシートに記入する内容を記憶する為の2次元配列resultを定義 事業所ごとに繰り返し { 一番最後の行まで繰り返し { もしresultにまだ無い顧客名なら { 顧客名が同じ行を全て探して、売上をまとめる resultの末尾に顧客情報を追加 } } } resultの内容を新しいシートに転記

もし分からないことがあれば、コメント欄などで聞いて頂ければお答えします。

投稿2020/11/10 11:53

Automatic9045

総合スコア313

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

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

hamada1110

2020/11/13 01:42

>Automatic9045 様 回答ありがとうございます。 全体の流れがもやっとしておりましたが、処理の方法など全体を可視化していただき 全体像がなんとなくですが掴めて参りました。 ココ数日でもう少し詰めていきたいと思います。 ありがとうございます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.47%

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

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

質問する

関連した質問