VB.NETで売上伝票を店舗別に集計してみよう(総合演習)
ここまでの VB.NET レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。クラス・Enum・List(Of T)・Dictionary(Of K, V)・For Each・Select Case——すでに練習したものだけで組み立てます。
作るのは売上伝票の集計です。伝票を一覧にし、店舗ごとに合計し、いちばん売れた店を見つけて、表の形にまとめます。
VB.NETがいまも現役なのは、こういう仕事があるからです。業務システムの集計や帳票の出力——「データを読んで、まとめて、表にする」という仕事で、VB.NETは長く使われてきました。この演習は、その骨格そのものです。
このページではコードを実行していません。VB.NETはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際にコンパイルして走らせた出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 今週の売上 ===== 1日 本店 ドリンク 480円 x12 = 5,760円 1日 駅前店 フード 1,280円 x 4 = 5,120円 2日 本店 雑貨 3,200円 x 2 = 6,400円 2日 本店 ドリンク 480円 x 9 = 4,320円 3日 駅前店 ドリンク 520円 x15 = 7,800円 3日 郊外店 フード 980円 x 7 = 6,860円 4日 駅前店 雑貨 3,200円 x 1 = 3,200円 4日 郊外店 ドリンク 520円 x 6 = 3,120円 5日 本店 フード 1,280円 x 5 = 6,400円 ---------------------------------------------- --- 店舗別 --- 本店 22,880円 好調 駅前店 16,120円 郊外店 9,980円 ---------------------------------------------- 伝票枚数: 9枚 売上合計: 48,980円 平均単価: 5,442円 最も売れた店舗: 本店(22,880円)
STEP 1伝票をクラスで表す
🎯 このステップのゴール: Sub Newで値を受け取り、金額を返すFunctionを持たせる
まずデータの形を決めます。伝票1枚が持つのは「日付・店舗・区分・単価・数量」の5つです。
Class Slipの中にPublicで並べ、Sub Newで受け取ります。Sub NewがVB.NETのコンストラクタ——オブジェクトを作るときに一度だけ呼ばれる特別な手続きです。
Me.Day = dayのMeは「このオブジェクト自身」を指します。引数のdayとフィールドのDayを区別するために付けています。VBは大文字小文字を区別しないので、Meが無いと自分自身への代入になってしまいます。
ここはVB.NET特有の注意点です。C#ならdayとDayは別物ですが、VBでは同じ名前とみなされます。Me.を付ける習慣をつけてください。
そしてPublic Function Amount()——単価×数量を返すメソッドです。
この1つを持たせるかどうかで、後の書きやすさが変わります。金額が必要になるたびにs.Price * s.Countと書いていたら、その計算が何か所にも散らばります。消費税を足すことになったとき、全部探すはめになります。
「そのデータについての計算は、そのデータの近くに置く」——クラスを使ういちばんの理由がこれです。
なお実務ではPublic ReadOnly Propertyを使うことが多いのですが、ここではレッスンで扱ったPublicフィールドの形に揃えています。
伝票枚数: 9枚 1日 本店 単価480円 x12 金額: 5760円
STEP 2For Eachで売上を合計する
🎯 このステップのゴール: リストを回して積み上げ、整数の割り算に気をつける
売上を全部足します。For Each s As Slip In slips——1枚ずつ取り出して、金額を足していきます。
As Slipと型を書いているのに注目してください。省略もできますが、書いておくとその場で型が確定します。s.と打ったときに候補が出ますし、打ち間違いもすぐ分かります。
足し始める前にDim grandTotal As Integer = 0と入れ物を用意するのを忘れないでください。
そして平均単価を出すところに、VB.NET特有の落とし穴があります。
grandTotal / slips.Countと書くと——結果が小数になります。VBの/は常に小数の割り算だからです。
ここが多くの言語と違います。CやC#やJavaでは、整数どうしを/で割ると整数が返ります。VBは逆で、整数どうしでも小数になります。
整数のまま割りたいときは\(バックスラッシュ)を使います。grandTotal \ slips.Countなら小数点以下が切り捨てられた整数が返ります。
この2つを取り違えると、金額に小数点が出たり、逆に端数が消えたりします。「/は小数、\は整数」——VBを書くなら覚えておく価値のある区別です。
売上合計: 48980円 平均単価(整数の割り算 \): 5442円 平均単価(小数の割り算 /): 5442.222222222223円
STEP 3EnumとSelect Caseで区分を振り分ける
🎯 このステップのゴール: 取りうる区分を限定し、ラベルを1か所で決める
商品の区分をStringで持つと、"ドリンク"と"drink"が混ざります。集計したときに別物として数えられて、合いません。
Enum CategoryでDrink・Food・Goodsの3つに限定します。それ以外は書けません——打ち間違えればコンパイルが通らないので、実行する前に気づけます。
日本語のラベルを付けるのがKindLabelです。Select Caseで振り分けます。
VBのSelect CaseにはBreakが要りません。C言語やJavaのように、書き忘れて次の分岐に流れ込む事故が起きない設計です。
Case Elseを必ず書いてください。「そのどれでもなかったとき」を受け止める場所です。いまは3つしかないので通りませんが、あとで区分を1つ足したとき、ここが最後の砦になります。
ラベルを関数に切り出しておく意味を、はっきりさせておきます。「ドリンク」を「飲料」に変えたい——直すのはこの関数の1行だけです。もし表示するたびにIfで書き分けていたら、書いた場所を全部探すことになります。
「同じ判断を2回書かない」——プログラムを保ちやすくする、いちばん基本の考え方です。
1日 本店 ドリンク 1日 駅前店 フード 2日 本店 雑貨 2日 本店 ドリンク 3日 駅前店 ドリンク 3日 郊外店 フード 4日 駅前店 雑貨 4日 郊外店 ドリンク 5日 本店 フード
STEP 4Dictionaryで店舗別に集計する
🎯 このステップのゴール: キーの有無を確かめてから足し、決まった順で取り出す
店舗ごとに売上をまとめます。使うのはDictionary(Of String, Integer)——店舗名をキーに、売上を値に持つ対応表です。
集計の中身に必ず要る一手間があります。
If Not totals.ContainsKey(s.Shop) Then totals(s.Shop) = 0
まだ無いキーにいきなり+=すると、その店舗の初回で落ちます。無いものを読もうとした、というエラーです。だから「無ければ0を入れてから足す」という順にします。
この形はVB.NETの定番なので、そのまま覚えてしまってください。Dictionaryで数え上げるときは、ほぼ必ずこの3行が出てきます。
取り出すところにも同じ注意が要ります。If totals.ContainsKey(shop) Then amount = totals(shop)——1枚も伝票の無い店舗を引くと落ちるからです。
そして表示順です。Shopsという配列を別に用意して、その順で回しています。
Dictionaryは順番を保証しません。入れた順でも五十音順でもなく、実行するたびに変わることがあります。「本店・駅前店・郊外店」という決まった順で見せたいなら、その順番を自分で持つしかありません。
これは言語を問わず踏む落とし穴です。手元では正しく見えたのに、本番で並びが変わって「表がおかしい」と言われる——覚えておいてください。
いちばん売れた店を探すのは「暫定の1位を置いて、勝ったら入れ替える」という型です。bestAmountを-1から始めているので、売上0円の店でも1位になれます。
本店: 22880円 駅前店: 16120円 郊外店: 9980円 空港店: 0円
STEP 5String.Formatで表に整える
🎯 このステップのゴール: 位置と幅と書式を指定して、桁を揃える
数字が並んでいるだけでは読めません。表の形に整えます。
使うのはString.Formatです。{0,2}のような書き方をします。
読み方は3つに分かれます。0が「何番目の引数か」、,2が「幅」、:#,0が「書式」です。
幅はプラスが右揃え、マイナスが左揃えです。{1,-5}なら5文字ぶんで左揃え——文字は左、数字は右に揃えると読みやすくなります。
#,0が金額の書式です。21440が21,440になります。3桁ごとの区切りを自分で書く必要はありません。
#と0の違いも覚えておくと役に立ちます。0は「値が無くても0を出す」、#は「無ければ出さない」。だから#,0は「3桁区切りで、最低1桁は出す」という意味になります。
区切り線はNew String("-"c, 46)です。末尾のcが「これは文字1つであって、文字列ではない」という印——VB.NET特有の書き方で、これが無いと型が合わずに怒られます。
日本語は文字の幅が英数字の2倍で表示されるので、{1,-5}でも見た目は完全には揃いません。大きくは整うので実用になりますが、厳密に揃えたいなら帳票の仕組みを使うのが本筋です。
---------------------------------------------- 1日 本店 ドリンク 480円 x12 = 5,760円 1日 駅前店 フード 1,280円 x 4 = 5,120円 2日 本店 雑貨 3,200円 x 2 = 6,400円 2日 本店 ドリンク 480円 x 9 = 4,320円 3日 駅前店 ドリンク 520円 x15 = 7,800円 3日 郊外店 フード 980円 x 7 = 6,860円 4日 駅前店 雑貨 3,200円 x 1 = 3,200円 4日 郊外店 ドリンク 520円 x 6 = 3,120円 5日 本店 フード 1,280円 x 5 = 6,400円 ----------------------------------------------
STEP 6ひとつのSubにまとめて仕上げる
🎯 このステップのゴール: Reportにまとめ、好調な店舗に印を付ける
最後の仕上げです。ここまでの部品をReportという1つのSubにまとめます。
SubとFunctionの違いを、ここで確かめておきます。Functionは値を返すもの、Subは返さないもの。Reportは表示するのが仕事なのでSubです。
この区別は、読む人への合図になります。Subと書いてあれば「戻り値を期待しなくていい」と分かります。C系の言語でvoidと書くのと同じ役割です。
好調な店舗に印を付けます。If amount >= GoodSales Then mark = " 好調"——基準を定数GoodSalesにしてあるのが要点です。
20000を直接書いてはいけません。読んだ人に「この2万円は何の2万円か」が分かりませんし、基準を変えたいときに探し回ることになります。
並べる順番にも意味を持たせます。まず伝票1枚ずつ、次に店舗別、最後に全体のまとめ——細かいものから大きいものへという流れです。帳票はたいていこの順に並んでいます。
完成です。GoodSalesを15000に変えてみてください。「好調」の付く店舗が増えます。10000にすれば3店舗とも付きます。
1つの数字を変えるだけで、判定が全部連動する——定数に切り出しておいた効果が、ここで確かめられます。
次に進むなら、この伝票をCSVから読むところです。LoadSlipsの中身を差し替えるだけで、Reportはそのまま使えます。データの作り方と使い方を分けておいた意味が、そこで効いてきます。
===== 今週の売上 ===== 1日 本店 ドリンク 480円 x12 = 5,760円 1日 駅前店 フード 1,280円 x 4 = 5,120円 2日 本店 雑貨 3,200円 x 2 = 6,400円 2日 本店 ドリンク 480円 x 9 = 4,320円 3日 駅前店 ドリンク 520円 x15 = 7,800円 3日 郊外店 フード 980円 x 7 = 6,860円 4日 駅前店 雑貨 3,200円 x 1 = 3,200円 4日 郊外店 ドリンク 520円 x 6 = 3,120円 5日 本店 フード 1,280円 x 5 = 6,400円 ---------------------------------------------- --- 店舗別 --- 本店 22,880円 好調 駅前店 16,120円 郊外店 9,980円 ---------------------------------------------- 伝票枚数: 9枚 売上合計: 48,980円 平均単価: 5,442円 最も売れた店舗: 本店(22,880円)
よくある質問
このページではコードを実行しないのですか?
していません。VB.NETはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際にコンパイルして走らせた出力を、そのまま載せています。
/ と \ は何が違いますか
答えが小数になるか、整数になるかです。
VBの/は整数どうしを割っても小数になります。7 / 2は3.5です。
\は整数の割り算で、小数点以下を切り捨てます。7 \ 2は3です。
ここは多くの言語と逆なので注意してください。CやC#やJavaでは/が整数の割り算になります。他の言語から来た人が、金額の計算で端数の扱いを間違えるのは、たいていこの違いが原因です。
Me. は必ず書かないといけませんか
引数とフィールドの名前が同じときは必須です。
VBは大文字小文字を区別しません。だから引数のdayとフィールドのDayは同じ名前とみなされます。Day = dayと書くと、自分自身に代入するだけで何も起きません。
Me.Day = dayと書けば、左が確実にフィールドを指します。
名前が違えば省略できますが、Sub Newの中では常に付ける習慣にしておくほうが安全です。
Dictionary の表示順が実行するたびに変わります
Dictionaryは順番を持たない入れ物だからです。キーから値を素早く引くことに特化していて、並び順は保証されません。
決まった順で表示したいなら、ステップ4のように並び順を配列として自分で持ちます。Shopsを回せば、何度実行しても同じ順です。
「本店・駅前店・郊外店」のように意味のある順に並べたいときは、並べ替えより自分で持つほうが素直です。
🚩 次はこのカテゴリへ
VB.NETとC#は同じ.NETの上で動く、双子のような言語です。List(Of T)もDictionary(Of K, V)も、C#ではList<T>・Dictionary<K, V>——書き方が違うだけで、中身は同じものです。C#カテゴリの最終演習では勤怠を集計します。同じ集計をLINQで短く書く方法も見られます。