An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

Python学習メモ - その4

まえがき

最近、Python を勉強し始めたので、その学習メモです。

今回はビット演算、比較演算、論理演算についてです。


ビット演算

ビット演算はC言語などとほぼ変わらないので、特別異なる点のみ説明します。


シフト演算
print(2 << 3)
print(4 >> 2)

実行結果

16
1


2進数で表示してみると、どう動いてるのかがよく分かると思います。

print(bin(2) + " << " + str(3) + " = " + bin(2 << 3))
print(bin(4) + " >> " + str(2) + " = " + bin(4 >> 2))

実行結果

0b10 << 3 = 0b10000
0b100 >> 2 = 0b1


ビット単位の AND
print(1 & 1)
print(1 & 0)
print(0 & 1)
print(0 & 0)

実行結果

1
0
0
0


ビット単位の OR
print(1 | 1)
print(1 | 0)
print(0 | 1)
print(0 | 0)

実行結果

1
1
1
0


ビット単位の XOR
print(1 ^ 1)
print(1 ^ 0)
print(0 ^ 1)
print(0 ^ 0)

実行結果

0
1
1
0


ビット反転

Pythonには整数のビット数制限がないので、C言語などと同じ感覚で使用すると問題が起こることがあるかもしれません。 Pythonでのビット反転は必ず-(x+1)となります。

print(~1)

実行結果

-2


比較演算

比較演算もC言語などと変わらないので、詳細は省略します。 なお、Pythonでは比較演算の結果はbool型(True / False)で返ります。

算術等価
print(1 == 1)
print(1 == 2)

実行結果

True
False


算術非等価
print(1 != 1)
print(1 != 2)

実行結果

False
True


小なり
print(1 < 1)
print(1 < 2)

print()

print(1 <= 1)
print(1 <= 2)

実行結果

False
True

True
True


大なり
print(1 > 1)
print(1 > 2)

print()

print(1 >= 1)
print(1 >= 2)

実行結果

False
False

True
False


論理演算

論理演算はC言語などと比べて機能的には変わりませんが、表記が異なります。

機能 Python C系言語 備考
論理否定 not !
論理等価 is ==
論理積 and &&
論理和 or ||


論理否定
print(not True)
print(not False)

実行結果

False
True


論理等価
print(True is True)
print(True is False)

実行結果

True
False


論理積
print(True and True)
print(True and False)
print(False and True)
print(False and False)

実行結果

True
False
False
False


論理和
print(True or True)
print(True or False)
print(False or True)
print(False or False)

実行結果

True
True
True
False


参考文献

Python 公式リファレンス

https://docs.python.org/ja/3.7/index.html

Python-izm

https://www.python-izm.com/

ゲームを作りながら楽しく学べるPythonプログラミング

エキスパートPythonプログラミング

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

Python学習メモ - その3

まえがき

最近、Python を勉強し始めたので、その学習メモです。

今回は基数についてです。

基数

基数は、各桁を構成する数値や記号の数を表し、簡単に言うと桁が繰り上がる数です。
この基数を使った数の表現方法を記数法と言います。


例えば、普段生活で使用するお金や数字の計算は 10 進数で、基数は 10(0〜9)がとなります。
また、時間の "分"は 60 進数で基数は 60(0〜59)ですし、"時"は 24 進数で基数は 24(0〜23)となります。

コンピュータの世界では、2 進数や 16 進数で数を表すと便利なことが多くあります。
また、最近ではあまり使われませんが 8 進数もあります。

2 進数、8 進数、16 進数

Python では、普通に整数値を記述した場合は 10 進数としてみなされます。
2 進数や 16 進数で表したい場合には、接頭語(プレフィックス)をつけます。

基数 接頭語 構成数値/記号 備考
10 進数 なし 0〜9
2 進数 0b 0〜1
8 進数 0o 0〜7
16 進数 0x 0〜9、A〜F


dec_num = 10
bin_num = 0b1010
oct_num = 0o12
hex_num = 0x0A

print(dec_num)
print(bin_num)
print(oct_num)
print(hex_num)

実行結果

10
10
10
10


2 進数や 16 進数で表現したとしても、内部的にはすべて整数型(int)で認識されます。
そのため、異なる基数で定義した数値同士の演算もそのまま行うことができます。

print(dec_num + bin_num + oct_num + hex_num)

実行結果

40


桁区切り

Python では、長い桁の数値を表す時に、見やすいように桁を区切る事ができます。
桁区切りは、区切りたい場所にアンダースコア"_"を挿入します。


2 進数は 4 桁ごと、16 進数は 2 桁ごと、10 進数は 3 桁ごとで区切られるのが一般的です。

bin_num1 = 0b10101010
bin_num2 = 0b1010_1010

hex_num1 = 0x1010
hex_num2 = 0x10_10

dec_num1 = 100000
dec_num2 = 100_000

print(bin_num1)
print(bin_num2)

print()

print(hex_num1)
print(hex_num2)

print()

print(dec_num1)
print(dec_num2)

実行結果

170
170

4112
4112

100000
100000


文字列変換

整数値を 2 進数、8 進数、16 進数の文字列に変換したい場合には、bin()、oct()、hex()関数を使用します。
文字列に変換した場合、プレフィックスは自動的に付加されます。

dec_num = 10

print(bin(dec_num))
print(oct(dec_num))
print(hex(dec_num))

実行結果

0b1010
0o12
0xa


数値変換

逆に、2 進数、8 進数、16 進数で表現された文字列を整数に変換する場合は、int()関数を使用します。

10 進数以外の文字列を数値に変換する場合には、第 2 引数に基数を指定する必要があります(第 2 引数を省略した場合は、10 進数値とみなして変換処理を行います)。

bin_str = "1010"
oct_str = "12"
hex_str = "0A"

print(int(bin_str, 2))
print(int(oct_str, 8))
print(int(hex_str, 16))

実行結果

10
10
10


ただし、変換したい文字列にプレフィックスがついていることが確実である場合、第 2 引数に 0 を指定することで、プレフィックスに応じた基数で整数への変換が行われます。

bin_str = "0b1010"
oct_str = "0o12"
hex_str = "0x0A"

print(int(bin_str, 0))
print(int(oct_str, 0))
print(int(hex_str, 0))

実行結果

10
10
10


参考文献

Python 公式リファレンス

https://docs.python.org/ja/3.7/index.html

Python-izm

https://www.python-izm.com/

ゲームを作りながら楽しく学べるPythonプログラミング

エキスパートPythonプログラミング

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

Python学習メモ - その2

まえがき

最近、Pythonを勉強し始めたので、その学習メモです。

今回は数値演算、数学関数についてです。


数値演算

まずは、数値演算です。
Pythonでは、標準で整数、浮動小数点数複素数を取り扱うことができます。


整数や浮動小数点は、C / C++ / C#などと違いビット数によって型を使い分ける必要がありません。
ちなみに、Python 3における整数と浮動小数点の精度は次のようになっているそうです。

種別 型名 ビット数 詳細 備考
整数 int なし 最大/最小の上限なし メモリが許す限り大きな値を取扱可能
浮動小数点数 float 64bit Cのdouble相当の精度


また、C / C++ / C#では標準で用意されていない複素数が使えることも特徴です。


整数(Integer Number)

Pythonでは、小数点をつけない数値を記述することで、整数として認識されます。
整数は、標準演算子で加算( + )、減算( - )、乗算( * )、除算( / or // )、剰余算( % )、累乗算( ** )を行うことができます。

除算は小数点以下も結果に含める"/"と、小数点以下を切り捨てる"//"を使い分けることができます。

int_num1 = 100
int_num2 = 3

add_num = int_num1 + int_num2
sub_num = int_num1 - int_num2
mul_num = int_num1 * int_num2
div_num1 = int_num1 / int_num2
div_num2 = int_num1 // int_num2
mod_num = int_num1 % int_num2
pow_num = int_num1 ** int_num2

print("Add : " + str(add_num))
print("Sub : " + str(sub_num))
print("Mul : " + str(mul_num))
print("Div : " + str(div_num1))
print("Div : " + str(div_num2))
print("Mod : " + str(mod_num))
print("Pow : " + str(pow_num))

実行結果

Add : 103
Sub : 97
Mul : 300
Div : 33.333333333333336
Div : 33
Mod : 1
Pow : 1000000


浮動小数点数(Floating Number)

また、小数点をつけると浮動小数点数として認識されます。
整数部分が0の場合は、省略することもできます。
浮動小数点数も整数と同様、加算( + )、減算( - )、乗算( * )、除算( / or // )、剰余算( % )、累乗算( ** )を行うことができます。

float_num1 = 3.14
float_num2 = .5

add_num = float_num1 + float_num2
sub_num = float_num1 - float_num2
mul_num = float_num1 * float_num2
div_num1 = float_num1 / float_num2
div_num2 = float_num1 // float_num2
mod_num = float_num1 % float_num2
pow_num = float_num1 ** float_num2

print("Add : " + str(add_num))
print("Sub : " + str(sub_num))
print("Mul : " + str(mul_num))
print("Div : " + str(div_num1))
print("Div : " + str(div_num2))
print("Mod : " + str(mod_num))
print("Pow : " + str(pow_num))

実行結果

Add : 3.64
Sub : 2.64
Mul : 1.57
Div : 6.28
Div : 6.0
Mod : 0.14000000000000012
Pow : 1.772004514666935


複素数(Complex Number)

「実数 + 虚数j」の形式で記述することで複素数として認識されます。
複素数は、加算( + )、減算( - )、乗算( * )、除算( / )を行うことができます。

complex_num1 = 10 + 8j
complex_num2 = 3 + 2j

add_num = complex_num1 + complex_num2
sub_num = complex_num1 - complex_num2
mul_num = complex_num1 * complex_num2
div_num = complex_num1 / complex_num2

print("Add : " + str(add_num))
print("Sub : " + str(sub_num))
print("Mul : " + str(mul_num))
print("Div : " + str(div_num))

実行結果

Add : (13+10j)
Sub : (7+6j)
Mul : (14+44j)
Div : (3.5384615384615383+0.3076923076923079j)


文字列 -> 数値変換

文字列から数値への変換は、int()、float()、complex()関数(コンストラクタ?)を使用します。
複素数文字列から複素数へ変換する場合は、余計な空白は入れてはいけないようです。("1 + 2j"など)

str_num1 = "100"
str_num2 = "3.14"

result_num = int(str_num1) * float(str_num2)

print(str_num1 + " * " + str_num2 + " = " + str(result_num))

str_num3 = "1+2j"

print(complex(str_num3))

実行結果

100 * 3.14 = 314.0


数学関数

数学関数を使用するには、mathライブラリをインポートする必要があります。

import math

数学関数は使用できる関数が非常に多いので、よく使いそうなものだけをピックアップして紹介します。


定数

よく使用される数学定数です。

print("π = " + str(math.pi))

print("e = " + str(math.e))

print("τ = " + str(math.tau))

print("∞ = " + str(math.inf))

print("NaN = " + str(math.nan))

実行結果

π = 3.141592653589793
e = 2.718281828459045
τ = 6.283185307179586
∞ = inf
NaN = nan


絶対値(fabs)
print("|3.5| = " + str(math.fabs(3.5)))
print("|-3.5| = " + str(math.fabs(-3.5)))

実行結果

|3.5| = 3.5
|-3.5| = 3.5


eの指数関数(exp)
print("e^2 = " + str(math.exp(2)))

実行結果

e^2 = 7.38905609893065


累乗(pow)
print("2^3 = " + str(math.pow(2, 3)))

実行結果

2^3 = 8.0


対数(log)
print("log2(8) = " + str(math.log(8, 2)))

print("log10(100) = " + str(math.log10(100)))

実行結果

log2(8) = 3.0
log10(100) = 2.0


平方根(sqrt)
print("√3 = " + str(math.sqrt(3)))

実行結果

√3 = 1.7320508075688772


床関数(floor)

床関数は実数xに対して、x以下の最大の整数を返します。

print("floor : 3.14 -> " + str(math.floor(3.14)))

実行結果

floor : 3.14 -> 3


天井関数(ceil)

天井関数は実数xに対して、x以上の最小の整数を返します。

print("ceil : 3.14 -> " + str(math.ceil(3.14)))

実行結果

ceil : 3.14 -> 4


切り捨て(trunc)

切り捨て関数は、小数点以下を切り捨てた整数を返します。

print("trunc : 3.14 -> " + str(math.trunc(3.14)))

実行結果

trunc : 3.14 -> 3


三角関数
print("sin(π/2) = " + str(math.sin(math.pi / 2)))

print("cos(π) = " + str(math.cos(math.pi)))

print("tan(π/4) = " + str(math.tan(math.pi * 0.25)))

print("asin(1) = " + str(math.asin(1)))

print("acos(1) = " + str(math.acos(1)))

print("atan(1) = " + str(math.atan(1)))

実行結果

sin(π/2) = 1.0
cos(π) = -1.0
tan(π/4) = 0.9999999999999999
asin(1) = 1.5707963267948966
acos(1) = 0.0
atan(1) = 0.7853981633974483


角度変換
print("180[deg] -> " + str(math.radians(180)) + "[rad]")

print("π[rad] -> " + str(math.degrees(math.pi)) + "[deg]")

実行結果

180[deg] -> 3.141592653589793[rad]
π[rad] -> 180.0[deg]


参考文献

Python 公式リファレンス

https://docs.python.org/ja/3.7/index.html

Python-izm

https://www.python-izm.com/

ゲームを作りながら楽しく学べるPythonプログラミング

エキスパートPythonプログラミング

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

Python学習メモ - その1

まえがき

最近、Pythonを勉強し始めたので、その学習メモです。


Hello, World

まずは、おなじみの「Hello, World」から。

print('Hello, World')

実行結果

Hello, World

余計なものは何もいらない。そうPythonならね。


文字列処理

続いてはよく使う文字列処理です。


文字列リテラル

Pythonでは、シングルクォート('')とダブルクォート("")のどちらでも文字列と認識されます。

print('Single Quote String')
print("Double Quote String")

実行結果

Single Quote String
Double Quote String


複数行文字列

文字列を3連クォートでくくることによって、複数行の文字列を定義することができます。

multiline_str = """line1
line2
line3
"""

print(multiline_str)

実行結果

line1
line2
line3


もしくは、改行文字("\n")を含めることでも複数行の文字列を定義することができます。

multiline_str = "line1\nline2\nline3"

print(multiline_str)

実行結果

line1
line2
line3


文字列連結

文字列の連結は"+"演算子で行うことができます。

concat_str1 = "con" + "cat" + "_" + "str"

print(concat_str1)

実行結果

concat_str


変数に格納した文字列に、"+"演算子で次々に連結して行くこともできます。

concat_str2 = "con"
concat_str2 = concat_str2 + "cat"
concat_str2 = concat_str2 + "_"
concat_str2 = concat_str2 + "str"

print(concat_str2)

実行結果

concat_str


また、"+="演算子を使用することで、同じ変数に対して次々と連結していくことができます。

concat_str2 = "012"
concat_str2 += "34"
concat_str2 += "567"
concat_str2 += "8"
concat_str2 += "9"

print(concat_str2)

実行結果

0123456789


繰り返し

文字列の繰り返しは"*"演算子で繰り返し回数をかけることで定義できます。

repeat_str1 = "abc" * 3

print(repeat_str1)

実行結果

abcabcabc


文字列の連結("+")と繰り返し("*")を組み合わせて行うこともできます。

repeat_str2 = ("test" + "_") * 5 + "test"

print(repeat_str2)

実行結果

test_test_test_test_test_test


文字列変換

数値などを文字列に変換する場合には、str()関数を使用します。

int_value = 100
int_str = str(int_value) + "%"

print(int_str)

float_value = 3.14
float_value = "π = " + str(float_value)

print(float_value)

実行結果

100%
π = 3.14


大文字/小文字変換

文字列のアルファベットをすべて大文字にするためにはupper()メソッドを使用します。
また、すべて小文字にするためにはlower()メソッドを使用します。

input_str = "TesT"

print(input_str.upper())
print(input_str.lower())

実行結果

TEST
test


置換

文字列を置換するためには、replace()メソッドを使用します。

input_str = "Hello, World"

print(input_str)

replace_str = input_str.replace("World", "Japan")

print(replace_str)

実行結果

Hello, World
Hello, Japan


分割

文字列を特定の区切り文字で分割するためには、split()メソッドを使用します。

分割された文字列はlist 型で返されます。

input_str = "a,b,c,d,e,f"

print(input_str)

split_str = input_str.split(",")

print(split_str)

実行結果

a,b,c,d,e,f
['a', 'b', 'c', 'd', 'e', 'f']


桁揃え

数値などの桁揃えを行いたいときは、rjust()メソッド、ljust()メソッドを使用します。

rjust() / ljust()メソッドの引数には、揃えたい桁数と桁揃え時に埋め込む文字を指定します。

例えば、10桁に満たない数値の左端を0で埋めたい場合には、以下のようにrjust()メソッドを使用します。

input_str = "1234"

print(input_str.rjust(10, "0"))

実行結果

0000001234


0埋めなどをせず、単純な左揃え、右揃えをしたい場合には、空白を指定してrjust()メソッド、ljust()メソッドを使用します。

input_str = "1234"

print("|" + input_str.rjust(5, " ") + "|")

print("|" + input_str.ljust(5, " ") + "|")

実行結果

| 1234|
|1234 |


0埋め

特定の文字埋めをせず、単純な0埋めのみをしたい場合には、zfill()メソッドを使用します。

input_str = "1234"

print(input_str.zfill(5))
print(input_str.zfill(3))

実行結果

01234
1234


検索

ある文字列が、特定の文字列から始まるかどうかを判定する場合には、startswith()メソッドを使用します。

input_str = "Hello, World"

print(input_str.startswith("Hello"))
print(input_str.startswith("World"))

実行結果

True
False


逆に、ある文字列が、特定の文字列で終わるかどうかを判定する場合には、endswith()メソッドを使用します。

input_str = "Hello, World"

print(input_str.endswith("Hello"))
print(input_str.endswith("World"))

実行結果

False
True


また、ある文字列に特定の文字列が含まれているかどうかを判定する場合には、"in"演算子を使用します。

input_str = "test"

print("e" in input_str)
print("a" in input_str)

実行結果

True
Flase


先頭/末尾の削除

文字列の先頭や末尾から特定の文字を削除したい場合には、lstrip()メソッド、rstrip()メソッドを使用します。
メソッドの引数に何も指定しない(引数を省略した)場合は、空白を除去します。
メソッドの引数に特定の文字列を指定した場合は、指定された文字列を除去します(lstrip()メソッドの場合は、指定文字列から始まる場合、rstrip()メソッドの場合は指定文字列で終わる場合のみ)。

input_str = "     1_test_1     "
print("|" + input_str + "|")

input_str = input_str.lstrip()
print("|" + input_str + "|")

input_str = input_str.lstrip("1_")
print("|" + input_str + "|")

input_str = input_str.lstrip("_1")
print("|" + input_str + "|")

print()

input_str = "     1_test_1     "
print("|" + input_str + "|")

input_str = input_str.rstrip()
print("|" + input_str + "|")

input_str = input_str.rstrip("_1")
print("|" + input_str + "|")

input_str = input_str.rstrip("1_")
print("|" + input_str + "|")

実行結果

|     1_test_1     |
|1_test_1     |
|test_1     |
|test_1     |

|     1_test_1     |
|     1_test_1|
|     1_test|
|     1_test|


ある文字列の、先頭および末尾に含まれる余分な空白を除去したい場合には、lstrip()メソッドとrstrip()メソッドを組み合わせて以下のように呼び出します。

input_str = "    test    "
print("|" + input_str + "|")

input_str = input_str.lstrip().rstrip()
print("|" + input_str + "|")

実行結果

|    test    |
|test|


参考文献

Python 公式リファレンス

https://docs.python.org/ja/3.7/index.html

Python-izm

https://www.python-izm.com/

ゲームを作りながら楽しく学べるPythonプログラミング

エキスパートPythonプログラミング

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

Excelの列名 <—> 列番号相互変換

まえがき

Excelの列名(A, B, ..., AA, AB)と列番号(1, 2, ...)を相互変換する方法のメモです。
ついでに、相互変換するWindows用アプリケーションも作成しました。

列名 <--> 列番号相互変換

共通定数

まずは、共通で使用する定数を定義します。

// アルファベットの文字数(26文字)
public const ulong AlphabetNum = ('Z' - 'A' + 1ul);

// 列名判定用正規表現パターン
public const string LabelPattern = @"^[A-Z]+$";

// 列番号判定用正規表現パターン
public const string IndexPattern = @"^[1-9][0-9]*$";


列名 --> 列番号

列名から列番号への変換は、アルファベット(A - Z)を1から26までの数値に置き換え、26進数のような形で計算することで算出できます。


 index = X_{n} * 26^{(n-1)} + X_{(n-1)}  * 26^{(n-2)}  + ... + X_{1} * 26^{0}
 n : \mbox{列名の桁数}
 X : \mbox{アルファベットを数値に変換した値}


Ex)

列名 計算式 結果
A  1 * 26^ 0 1
B  2 * 26^ 0 2
Z  26 * 26^ 0 26
AA  (1 * 26^ 1) + (1 * 26^ 0) 27
AB  (1 * 26^ 1) + (2 * 26^ 0) 28
ZZ  (26 * 26^ 1) + (26 * 26^ 0) 702


public static string ConvertLabelToIndex(string label)
{
    // 入力文字列が列名パターンにマッチしない
    if (!Regex.IsMatch(label, LabelPattern))
    {
        // 入力値エラー
        throw new ArgumentException($"無効なラベル名です : {label}");
    }
    else
    {
        // 出力用列番号初期化
        var index = 0ul;

        // 底の初期化(n = 26^x : x = 0)
        var n = 1ul;

        // 入力列名を文字配列に変換(ex : ABC -> A, B, C)
        var array = label.ToList();

        // 文字配列を反転(ex : A, B, C -> C, B, A)
        array.Reverse();

        // 反転した文字配列を1文字ずつ走査
        foreach (var c in array)
        {
            // 現在のアルファベット文字を数値に変換(A - Z : 1 - 26)
            var x = (ulong)(c - 'A') + 1ul;

            // アルファベットに対応する数値と底を乗算
            var y = x * n;

            // 乗算した結果を列番号に加算
            index += y;

            // 次の桁の底を算出(26^0 -> 26^1 -> 26^2)
            n *= ColumnIndexConverter.AlphabetNum;
        }

        // 列番号を文字列に変換して出力
        return $"{index}";
    }
}


列番号 --> 列名

列番号から列名への変換は、列名から列番号への変換と逆のことをやれば良いということになります。

  1. 列番号を入力値にセット( x = index)
  2. 入力値が0始まりになるように1減算( x = x - 1)
  3. 入力値とアルファベット文字数(26)の剰余を算出( m = Mod(x, 26))
  4. 算出した剰余(0 - 25)をアルファベット文字(A - Z)に変換( A = \mbox{'A'} + m)
  5. 変換したアルファベット文字を列名の先頭に追加([text: label = A + label])
  6. 入力値とアルファベット文字数の商を次の入力値にセット( x = x / 26)
  7. 入力値が0になるまで2〜5を繰り返す


Ex1) 列番号 = 1

  1.  x = 1
  2.  x = x - 1 = 0
  3.  m = Mod(x, 26) = Mod(0, 26) = 0
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 0 = \mbox{'A'}
  5.  label = \mbox{'A'} + label = \mbox{'A'} + \mbox{""} = \mbox{"A"}
  6.  x = x / 26 = 0 / 26 = 0
  7.  x = 0のため終了


Ex2) 列番号 = 2

  1.  x = 2
  2.  x = x - 1 = 1
  3.  m = Mod(x, 26) = Mod(1, 26) = 1
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 1 = \mbox{'B'}
  5.  label = \mbox{'B'} + label = \mbox{'B'} + \mbox{""} = \mbox{"B"}
  6.  x = x / 26 = 0 / 26 = 0
  7.  x = 0のため終了


Ex3) 列番号 = 28

  1.  x = 28
  2.  x = x - 1 = 27
  3.  m = Mod(x, 26) = Mod(27, 26) = 1
  4.  A = \mbox{'A'} + m = \mbox{'A'} + 1 = \mbox{'B'}
  5.  label = \mbox{'B'} + label = \mbox{'B'} + \mbox{""} = \mbox{"B"}
  6.  x = x / 26 = 27 / 26 = 1
  7.  x = x - 1 = 0
  8.  m = Mod(x, 26) = Mod(0, 26) = 0
  9.  A = \mbox{'A'} + m = \mbox{'A'} + 0 = \mbox{'A'}
  10.  label = \mbox{'A'} + label = \mbox{'A'} + \mbox{"B"} = \mbox{"AB"}
  11.  x = x / 26 = 0 / 26 = 0
  12.  x = 0のため終了


public static string ConvertIndexToLabel(string index)
{
    // 入力文字列が列番号パターンにマッチしない
    if (!Regex.IsMatch(index, IndexPattern))
    {
        // 入力値エラー
        throw new ArgumentException($"無効なインデックスです : {index}");
    }
    else
    {
        // 入力文字列を数値に変換
        var value = ulong.Parse(index);

        // 入力数値が0以下
        if (value <= 0)
        {
            // 入力値エラー
            throw new ArgumentException($"無効なインデックスです : {index}");
        }
        else
        {
            // 出力用列名を初期化
            var label = string.Empty;

            // アルファベット文字数(26)を取得
            var a = ColumnIndexConverter.AlphabetNum;

            // 入力値が0より大きい間繰り返す
            while (value > 0)
            {
                // 入力値を0始まりになるようにデクリメント
                value--;

                // 入力値の剰余を文字に変換
                var c = (char)('A' + (value % a));

                // 変換した文字を列名の先頭に追加
                label = $"{c}{label}";

                // 入力値とアルファベット文字数の商を次の入力値にセット
                value /= a;
            }

            // 列名を出力
            return label;
        }
    }
}


相互変換アプリケーション

CUI版と、GUI版を作成しました。
GitHubにアップしていますので、詳細はそちらをご覧ください。

f:id:an-embedded-engineer:20190504174446p:plain:w500
CUIアプリケーション


f:id:an-embedded-engineer:20190504174513p:plain:w500
GUIアプリケーション(列番号 → 列名)


f:id:an-embedded-engineer:20190504174615p:plain:w500
GUIアプリケーション(列名 → 列番号)

UMLのステートマシン図を実装する for C# - まとめ

まとめ

UMLのステートマシン図を実装する for C#」のまとめ記事です。
次のようなステートマシンをC#で実装する方法について紹介しています。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


No. 内容 Link 備考
その1 ステートマシンベースクラス実装 Link
その2 ステートマシンサンプル説明 Link
その3 ステートマシンサンプル実装(1) Link
その4 ステートマシンサンプル実装(2) Link
その5 エアコンモデル実装 Link
その6 エアコンアプリケーション実装(CUI) Link
その7 エアコンアプリケーション実装(GUI) Link


ソースコード一式

ソースコード一式はGitHubにアップしています。
https://github.com/an-embedded-engineer/StateMachineSample


Visual Studio 2017 / 2019にてビルド&動作確認しています。

UMLのステートマシン図を実装する for C# - その7

まえがき

ステートマシンをGUI(WPF)で動作させるアプリケーションを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


事前準備

今回は、WPFMVVMリアクティブプログラミングGUIアプリケーションを実装しようと思います。


MVVMのフレームワークにはLivetを使用します。 こちらからプロジェクトテンプレート(Visual Studio 2017用)をダウンロードして、インストールしてください。


インストールしたLivetプロジェクトテンプレートを使用してWPFアプリケーション用プロジェクトを作成します。


また、リアクティブプログラミングを実現するために、NuGetからReactive Extension(System.Reactive)ReactivePropertyWPFプロジェクトにインストールします。


NotificationObject クラス

MVVMではプロパティ(=データ)が変更されたことをUIに通知するためにINotifyPropertyChanged インタフェースを実装する必要があります。
そのため、INotifyPropertyChanged インタフェースを実装したベースクラスとして、NotificationObject クラスを実装します。

using System.ComponentModel;

public class NotificationObject : INotifyPropertyChanged
{
    // プロパティ変更イベントハンドラ
    public event PropertyChangedEventHandler PropertyChanged;

    // プロパティ変更イベント通知
    protected void RaisePropertyChanged(string name)
    {
        // プロパティ変更イベントハンドラ呼び出し(登録されていたら)
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}


プロパティ変更通知処理

エアコンステートマシンおよびエアコンモデルにおいて、GUIに表示するために、値の変更通知が必要なプロパティを変更します。 値の変更を通知できるようにするために、上記で実装したNotificationObjectを継承します。

StateMachine クラス

StateMachine クラスでは現在の状態を示すCurrentStateプロパティに、プロパティ変更イベント通知処理を組み込みます。

public abstract class StateMachine : NotificationObject


// 現在状態(データ本体)
private State _CurrentState { get; set; }

// 現在状態(プロパティ変更イベント通知用)
public State CurrentState
{
    get { return this._CurrentState; }
    set
    {
        // 現在の状態と異なる値が代入された
        if (this._CurrentState != value)
        {
            // 現在の状態を更新
            this._CurrentState = value;
            // プロパティ値の変更を通知
            this.RaisePropertyChanged(nameof(this.CurrentState));
        }
    }
}


AirConditioner クラス

同様に、AirConditioner クラスにおいても値の変更通知が必要なプロパティに、、プロパティ変更イベント通知処理を組み込みます。

public class AirConditioner : NotificationObject


// 現在温度(本体)
private int _Temperature { get; set; }

// 現在温度(プロパティ変更イベント通知用)
public int Temperature
{
    get { return this._Temperature; }
    set
    { 
        if (this._Temperature != value)
        {
            this._Temperature = value;
            this.RaisePropertyChanged(nameof(this.Temperature));
        }
    }
}

// 目標温度(本体)
private int _TargetTemperature { get; set; }

// 目標温度(プロパティ変更イベント通知用)
public int TargetTemperature
{
    get { return this._TargetTemperature; }
    set
    {
        if (this._TargetTemperature != value)
        {
            this._TargetTemperature = value;
            this.RaisePropertyChanged(nameof(this.TargetTemperature));
        }
    }
}

// 現在湿度(本体)
private int _Humidity { get; set; }

// 現在湿度(プロパティ変更イベント通知用)
public int Humidity
{
    get { return this._Humidity; }
    set
    {
        if (this._Humidity != value)
        {
            this._Humidity = value;
            this.RaisePropertyChanged(nameof(this.Humidity));
        }
    }
}


MainWindow クラス(Xaml)

プロジェクト生成時に自動的に生成されたMainWindow.xamlを編集してGUIの画面設計をしていきます。

<Window x:Class="StateMachineSample.WPF.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:StateMachineSample.WPF.Views"
        xmlns:vm="clr-namespace:StateMachineSample.WPF.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="ListBoxItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
        <Style TargetType="StatusBarItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/> <!-- Command Buttons -->
            <RowDefinition Height="Auto"/> <!-- Target Temp Slider -->
            <RowDefinition Height="Auto"/> <!-- Latest Message -->
            <RowDefinition Height="*"/> <!-- Message Logs -->
            <RowDefinition Height="Auto"/> <!-- Status -->
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- ストップボタン -->
            <Button Grid.Column="0" Content="Stop" Command="{Binding StopCommand}" />
            <!-- スタートボタン -->
            <Button Grid.Column="1" Content="Start" Command="{Binding StartCommand}" />
            <!-- 冷房ボタン -->
            <Button Grid.Column="2" Content="Cool" Command="{Binding CoolCommand}" />
            <!-- 暖房ボタン -->
            <Button Grid.Column="3" Content="Heat" Command="{Binding HeatCommand}" />
            <!-- 除湿ボタン -->
            <Button Grid.Column="4" Content="Dry" Command="{Binding DryCommand}" />
            <!-- クリーニングボタン -->
            <Button Grid.Column="5" Content="Clean" Command="{Binding CleanCommand}" />
        </Grid>
        <Grid Grid.Row="1">
            <StackPanel Orientation="Horizontal">
                <!-- 目標温度変更スライドバー -->
                <TextBlock Text="Target Temperature : " />
                <Slider Value="{Binding TargetTemperature.Value}" Width="100"
                        VerticalAlignment="Center"
                        Minimum="{Binding MinTargetTemperature}"
                        Maximum="{Binding MaxTargetTemperature}"/>
                <!-- 目標温度アップボタン -->
                <Button Content="Up" Command="{Binding UpCommand}" Width="80" />
                <!-- 目標温度ダウンボタン -->
                <Button Content="Down" Command="{Binding DownCommand}" Width="80" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="2">
            <StackPanel Orientation="Horizontal">
                <!-- 最新受信メッセージ -->
                <TextBlock Text="Message : " />
                <TextBlock Text="{Binding Message.Value}" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="3">
            <!-- 受信メッセージログ -->
            <ListBox ItemsSource="{Binding MessageLog}" Margin="5"
                     ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                     ScrollViewer.VerticalScrollBarVisibility="Visible"/>
        </Grid>
        <Grid Grid.Row="4">
            <StatusBar>
                <!-- 現在のステートマシン状態表示 -->
                <StatusBarItem Content="状態:" />
                <StatusBarItem Content="{Binding Status.Value}" />
                <Separator/>
                <!-- 目標温度表示 -->
                <StatusBarItem Content="目標温度:" />
                <StatusBarItem Content="{Binding TargetTemp.Value}" />
                <Separator/>
                <!-- 現在の温度表示 -->
                <StatusBarItem Content="温度:" />
                <StatusBarItem Content="{Binding Temperature.Value}" />
                <Separator/>
                <!-- 現在の湿度表示 -->
                <StatusBarItem Content="湿度:" />
                <StatusBarItem Content="{Binding Humidity.Value}" />
            </StatusBar>
        </Grid>
    </Grid>
</Window>


上記のようにxamlファイルを編集すると、以下のようなデザインの画面になります。

f:id:an-embedded-engineer:20190502232943p:plain:w500
GUIサンプル


MainWindowViewModel クラス

最後に、ViewModelの実装を行っていきます。
ViewModelはView(MainWindow)に描画するための状態やデータの保持と、Viewから受け取った入力(ボタン押下、テキスト入力など)を適切な形式に変換し、Model(エアコンモデル、ステートマシン)に伝達する役割を持ちます。


今回は、ViewとModelの仲介方法として、Reactive ExtensionおよびReactivePropertyを使用したリアクティブプログラミングを使用します。
リアクティブプログラミングを用いる事によって、以下のような処理の扱いが非常に簡単になります。

  • GUIによる入出力
  • 時間経過で状態が変換するもの
  • 非同期の通信処理


Model

MVVMで言うところのModelとは、アプリケーショのドメイン(問題領域)を解決するために、そのアプリケーションが扱う領域のデータと手続き(ビジネスロジック)を表現する要素のことを言います。
今回のサンプルでは、エアコンモデル(AirConditioner)やエアコンステートマシンが該当します。

// エアコンモデル
public AirConditioner Model { get; }

// エアコンステートマシン
public ModelStateMachine StateMachine { get; }


データバインディングプロパティ

WPFのデータバインディングによってGUIに表示するデータを保持するプロパティを定義します。
リアルタイムで値が変化するプロパティはReactivePropertyを使用します。
ReactivePropertyを使用することによって、データの値が変化したときにその変更がUIに通知され、Viewで表示している値に反映されます。

// 受信メッセージ
public ReactiveProperty<string> Message { get; }

// 受信メッセージログ
public ReadOnlyReactiveCollection<string> MessageLog { get; }

// 現在の状態
public ReactiveProperty<State> CurrentState { get; }

// 最大目標温度
public int MaxTargetTemperature { get; }

// 最小目標温度
public int MinTargetTemperature { get; }

// 目標温度
public ReactiveProperty<int> TargetTemperature { get; }

// 現在の状態(ステータスバー表示用)
public ReactiveProperty<string> Status { get; }

// 目標温度(ステータスバー表示用)
public ReactiveProperty<string> TargetTemp { get; }

// 現在の温度(ステータスバー表示用)
public ReactiveProperty<string> Temperature { get; }

// 現在の湿度(ステータスバー表示用)
public ReactiveProperty<string> Humidity { get; }


コマンド

ユーザからの入力を変換してModelに伝達するためのコマンドを定義します。
コマンドはReactiveCommandを使用します。
ReactiveCommandを使用することで、ReactivePropertyで変化したデータの値に応じてコマンドの使用可否(ボタンの有効/無効)などを自動的に切り替えることができます。

// ストップコマンド
public ReactiveCommand StopCommand { get; }

// スタートコマンド
public ReactiveCommand StartCommand { get; }

// 冷房コマンド
public ReactiveCommand CoolCommand { get; }

// 暖房コマンド
public ReactiveCommand HeatCommand { get; }

// 除湿コマンド
public ReactiveCommand DryCommand { get; }

// クリーニングコマンド
public ReactiveCommand CleanCommand { get; }

// 目標温度アップコマンド
public ReactiveCommand UpCommand { get; }

// 目標温度ダウンコマンド
public ReactiveCommand DownCommand { get; }


コンストラク

今回のサンプルは、すべての処理の定義がコンストラクタ内で完結します。 ReactivePropertyやReactiveCommandを使用することで、データの変化に対する処理やユーザからの入力に対する処理を「宣言的」に記述することができます。

// コンストラクタ
public MainWindowViewModel()
{
}


固定値設定

時間的に変化しない固定値プロパティをセットします。
今回は、目標温度のリミッタに使用する最大/最小値をセットします。

// 最大目標温度
this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

// 最小目標温度
this.MinTargetTemperature = AirConditioner.MinTargetTemperature;


ステータス情報初期化

ステータスバーに表示する各種情報(状態、温度、湿度など)を空の文字列を初期値とし、string型を保持するReactivePropertyとして宣言します。

this.Status = new ReactiveProperty<string>("");

this.TargetTemp = new ReactiveProperty<string>("");

this.Temperature = new ReactiveProperty<string>("");

this.Humidity = new ReactiveProperty<string>("");


受信メッセージロギング

ステートマシンから送信されるメッセージをロギングする処理を記述します。


最新受信メッセージは初期値空(null)のstring型を保持するReactivePropertyとして宣言します。
初期値をnullとして宣言する理由は、空文字("")にしてしまうと、受信メッセージログに変換した際に、空行が含まれてしまうためです。

// 最新受信メッセージ初期化
this.Message = new ReactiveProperty<string>();


受信メッセージログは、最新受信メッセージを、受け取った順番に保持するReactiveCollectionに変換して使用します。

// 最新受信メッセージをReactive Collectionに変換し、受信メッセージログにする
this.MessageLog = this.Message.ToReadOnlyReactiveCollection();


Messengerの受信イベントハンドラには、受信したメッセージ文字列を最新受信メッセージプロパティにセットする処理を記述します。
こうすることで、ステートマシンからメッセージが送信されると、最新受信メッセージプロパティに受信メッセージがセットされ、受信したメッセージがログとしてリスト化されるようになります。

using StmMessenger = StateMachineSample.Lib.Messenger;

// メッセージ受信イベントハンドラ登録
StmMessenger.OnMessageReceived += (message) =>
{
    // 最新受信メッセージに受信したメッセージ文字列をセット
    this.Message.Value = message;
};


Model初期化

今回のModelにあたるエアコンモデル(AirConditioner)とエアコンステートマシンのインスタンスを生成します。

// エアコンモデルインスタンス生成
this.Model = new AirConditioner();

// エアコンステートマシンインスタンス生成
this.StateMachine = new ModelStateMachine(this.Model);


Model - Viewデータ接続

ReactivePropertyを用いて、Modelの状態を監視し、ModelとViewのデータ接続を行います。


エアコンステートマシンの現在の状態を監視し、ReactivePropertyに変換します。
これによって、ステートマシンの状態が変化するたびにViewModelのCurrentStateが更新されます。
CurrentStateの値は、各種コマンドの実行可否(ボタンの有効/無効)制御に使用します。

// エアコンステートマシンの現在状態を監視し、ReactivePropertyに変換(Model -> View単方向)
this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();


エアコンモデルの目標温度を監視し、ReactivePropertyに変換します。 目標温度はModelからViewへの変更通知(プログラム上から目標温度変更)とViewからModelへの変更通知(ユーザ入力から目標温度変更)が必要となるため、双方向の監視が必要になります。

// エアコンモデルの目標温度を監視し、ReactivePropertyに変換(Model <-> View双方向)
this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);


コマンド宣言

ユーザからの入力を受け付けるコマンドを宣言します。
ReactiveCommandは、宣言時に実行可否を切り替える条件を指定することができます。
今回の場合は、実行可能な場合は、対応するボタンが押下可能な状態になり、実行不可能な状態になっている場合は、ボタンが押下不可能になることで、コマンドを実行できない状態になります。

// 現在の状態がRunning状態またはClean状態の時に実行可能なストップコマンドを宣言
this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

// 現在の状態がStop状態の時に実行可能なスタートコマンドを宣言
this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な冷房コマンドを宣言
this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な暖房コマンドを宣言
this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な除湿コマンドを宣言
this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能なクリーニングコマンドを宣言
this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の目標温度が最大目標温度より小さい場合に実行可能な目標温度アップコマンドを宣言
this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

// 現在の目標温度が最小目標温度より大きい場合に実行可能な目標温度ダウンコマンドを宣言
this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


コマンド処理定義

コマンドが実行された時の処理を定義します。
今回は、ステートマシンのトリガ送信や、エアコンモデルの目標温度変更を実行するようにします。

// ストップコマンドが実行されたら、エアコンステートマシンにSwitchStopトリガを送信
this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

// スタートコマンドが実行されたら、エアコンステートマシンにSwitchStartトリガを送信
this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

// 冷房コマンドが実行されたら、エアコンステートマシンにSwitchCoolトリガを送信
this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

// 暖房コマンドが実行されたら、エアコンステートマシンにSwitchHeatトリガを送信
this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

// 除湿コマンドが実行されたら、エアコンステートマシンにSwitchDryトリガを送信
this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

// クリーニングコマンドが実行されたら、エアコンステートマシンにSwitchCleanトリガを送信
this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

// 目標温度アップコマンドが実行されたら、エアコンモデルに目標温度アップを要求
this.UpCommand.Subscribe(_ => this.Model.Up());

// 目標温度ダウンコマンドが実行されたら、エアコンモデルに目標温度ダウンプを要求
this.DownCommand.Subscribe(_ => this.Model.Down());


周期処理

周期的にModelの状態を監視し、Viewに反映させる処理を実装します。
今回はReactive Extensionを使用して100msごとに呼び出されるインターバルタイマを作成し、ステータスバーに表示する各種状態値を更新と、ステートマシンの定常処理を実行するようにします。
Modelで保持されている状態値をユーザが見やす形式に変換する(単位をつける、いくつかの情報を結合するなど)のもViewModelの役割です。

// 100ms間隔のインターバルタイマ生成
var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

// タイマで周期実行する処理の定義
var timer_sub = interval.Subscribe(
    i =>
    {
        // ステートマシンの現在の状態がRunning状態
        if (this.StateMachine.CurrentState is RunningState running_state)
        {
            // サブステートマシンを取得
            var sub = running_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        // ステートマシンの現在の状態がClean状態
        else if (this.StateMachine.CurrentState is CleanState clean_state)
        {
            // サブステートマシンを取得
            var sub = clean_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        else
        {
            // 現在の状態(メイン状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState}";
        }

        // 目標温度を文字列に変換してセット
        this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Temperature.Value = $"{this.Model.Temperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Humidity.Value = $"{this.Model.Humidity}[%]";

        // ステートマシンの定常処理実行
        this.StateMachine.Update();
    });


全体

ソース全体を示します。

public class MainWindowViewModel : ViewModel
{
    // エアコンモデル
    public AirConditioner Model { get; }

    // エアコンステートマシン
    public ModelStateMachine StateMachine { get; }

    // 受信メッセージ
    public ReactiveProperty<string> Message { get; }

    // 受信メッセージログ
    public ReadOnlyReactiveCollection<string> MessageLog { get; }

    // 現在の状態
    public ReactiveProperty<State> CurrentState { get; }

    // 最大目標温度
    public int MaxTargetTemperature { get; }

    // 最小目標温度
    public int MinTargetTemperature { get; }

    // 目標温度
    public ReactiveProperty<int> TargetTemperature { get; }

    // 現在の状態(ステータスバー表示用)
    public ReactiveProperty<string> Status { get; }

    // 目標温度(ステータスバー表示用)
    public ReactiveProperty<string> TargetTemp { get; }

    // 現在の温度(ステータスバー表示用)
    public ReactiveProperty<string> Temperature { get; }

    // 現在の湿度(ステータスバー表示用)
    public ReactiveProperty<string> Humidity { get; }

    // ストップコマンド
    public ReactiveCommand StopCommand { get; }

    // スタートコマンド
    public ReactiveCommand StartCommand { get; }

    // 冷房コマンド
    public ReactiveCommand CoolCommand { get; }

    // 暖房コマンド
    public ReactiveCommand HeatCommand { get; }

    // 除湿コマンド
    public ReactiveCommand DryCommand { get; }

    // クリーニングコマンド
    public ReactiveCommand CleanCommand { get; }

    // 目標温度アップコマンド
    public ReactiveCommand UpCommand { get; }

    // 目標温度ダウンコマンド
    public ReactiveCommand DownCommand { get; }

    // コンストラクタ
    public MainWindowViewModel()
    {
        this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

        this.MinTargetTemperature = AirConditioner.MinTargetTemperature;

        this.Status = new ReactiveProperty<string>("");

        this.TargetTemp = new ReactiveProperty<string>("");

        this.Temperature = new ReactiveProperty<string>("");

        this.Humidity = new ReactiveProperty<string>("");

        this.Message = new ReactiveProperty<string>();

        this.MessageLog = this.Message.ToReadOnlyReactiveCollection();

        StmMessenger.OnMessageReceived += (message) =>
        {
            this.Message.Value = message;
        };

        this.Model = new AirConditioner();

        this.StateMachine = new ModelStateMachine(this.Model);

        this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();

        this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);

        this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

        this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

        this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

        this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


        this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

        this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

        this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

        this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

        this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

        this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

        this.UpCommand.Subscribe(_ => this.Model.Up());

        this.DownCommand.Subscribe(_ => this.Model.Down());


        var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

        var timer_sub = interval.Subscribe(
            i =>
            {
                if (this.StateMachine.CurrentState is RunningState running_state)
                {
                    var sub = running_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else if (this.StateMachine.CurrentState is CleanState clean_state)
                {
                    var sub = clean_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else
                {
                    this.Status.Value = $"{this.StateMachine.CurrentState}";
                }

                this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
                this.Temperature.Value = $"{this.Model.Temperature}[℃]";
                this.Humidity.Value = $"{this.Model.Humidity}[%]";

                this.StateMachine.Update();
            });
    }
}


まとめ

今回で、「C#UMLのステートマシン図の実装」は完成となります。


ソースコード一式はGitHubにアップしています。
https://github.com/an-embedded-engineer/StateMachineSample