ねこでもわかるかもしれないAiScript(Misskey Play)
AiScript is 何
ActivityPub対応SNSの「Misskey」を開発したsyuiloさんが開発したプログラミング言語で、Misskeyの一部機能(Play, プラグイン, ウィジェット)にも埋め込まれている。やつです。
今回はMisskey PlayのAiScriptの話だけします。
標準プリセットから見るAiScript
Misskey Playには標準でいくつかプリセットがあります。
Omikuji
おみくじのプリセットです。
/// @ 0.16.0
// ユーザーごとに日替わりのおみくじのプリセット
// 選択肢
let choices = [
"ギガ吉"
"大吉"
"吉"
"中吉"
"小吉"
"末吉"
"凶"
"大凶"
]
// シードが「ユーザーID+今日の日付」である乱数生成器を用意
let random = Math:gen_rng(`{USER_ID}{Date:year()}{Date:month()}{Date:day()}`)
// ランダムに選択肢を選ぶ
let chosen = choices[random(0 (choices.len - 1))]
// 結果のテキスト
let result = `今日のあなたの運勢は **{chosen}** です。`
// UIを表示
Ui:render([
Ui:C:container({
align: 'center'
children: [
Ui:C:mfm({ text: result })
Ui:C:postFormButton({
text: "投稿する"
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
])
/// @ 0.16.0
// ユーザーごとに日替わりのおみくじのプリセット
// 選択肢
/// @ 0.16.0
←これはAiScriptのバージョンです。
//
←これで始まる行はただのコメントです。
let choices = [
"ギガ吉"
"大吉"
"吉"
"中吉"
"小吉"
"末吉"
"凶"
"大凶"
]
ここでは、choices
変数を配列で初期化しています。
変数というのは名前をつけて覚えるためのもので、配列というのは要はリストのことです。
let なんちゃら = かんちゃら
とすることで、かんちゃらを「なんちゃら」という名前で覚えることができます。
ここでは["ギガ吉" "大吉" "吉" ...]
というリストを「choices
」という名前で覚えたわけです。
なぜ""で囲っているの?
AiScriptをはじめとするプログラミング言語では、型というシステムがあります。
例えばコンピューターに「1」と言った時、文字列としての「いち」なのか、数字としての「1」なのか理解できません。AiScriptでは、"1"
とダブルクォーテーション記号で囲った場合は文字列の「1」、囲わずに1
と言った場合には数字の1を指します。
また、'1'
も"1"
と同じように文字列として解釈されます。
// シードが「ユーザーID+今日の日付」である乱数生成器を用意
let random = Math:gen_rng(`{USER_ID}{Date:year()}{Date:month()}{Date:day()}`)
ここでは、AiScriptにあるMath:gen_rng()
という関数を使って、ランダムな数を生成する関数を用意し、random
という名前で覚えています。
関数というのは、何らかのデータを受け取ったり受け取らなかったりして、何か操作を行ったり加工したりするものです。
Math:gen_rng()
は、シード(種)の値を受け取って、ランダムな数生成器(関数)を返す関数です。
ここで注意すべきは、ここで作ったのはランダムな数生成器であって、ランダムな数そのものではないということです。実際にランダムな数を発生させるには、作ったもの(関数)にさらに「いくつからいくつまでの値を作るのか」という範囲を入力する必要がありますが、この行ではまだやりません。
コードでは、Math:gen_rng()
のシードになにやら「`」記号で囲まれたデータを入れています。「`」という記号は、AiScriptではデータを埋め込める文字列に使う記号です。{}
記号でデータを埋め込めます。たとえば、
let name = "たかし"
say(`Hello, {name}!!`)
とすると、say関数[1]には、実際には"Hello, たかし!!"
というデータが渡されます。nameとして覚えたデータを埋め込んでいるわけです。
ここで、もう一度コードを見てみます。
`{USER_ID}{Date:year()}{Date:month()}{Date:day()}`
USER_ID
というのは、何も言わなくても最初からユーザーIDが入っている変数です。[2]
Date:year()
というのは、現在の年を返す関数です。
Date:month()
というのは、現在の月を返す関数です。
Date:day()
というのは、現在の日を返す関数です。
つまり、2023/12/25にこのPlayを実行したユーザーIDAbCdEfGhIj
の場合は、"AbCdEfGhIj20231225"
と言っているのとおなじになるわけです。
さて、この値をMath:gen_rng()
に入力します。すると乱数生成器が作られます。この乱数生成器はユーザーIDと日付によって変化する、唯一無二のものです。この生成器を、random
という名前で覚えておきます。
// ランダムに選択肢を選ぶ
let chosen = choices[random(0 (choices.len - 1))]
この行では、choices
のrandom(0 choices.len - 1)
番目の値をchosen
として覚えています。
choices[~]
となっているのは、choices
配列の~番目という意味です。
random
はさっき覚えた乱数生成器です。乱数生成器は関数なので、()
に入力を入れられます。この入力に、0
からchoices.len - 1
という範囲を指定しています。choices.len
というのは、choices
配列の長さのことです。そこから1を引いています。
これで、0
からchoices.len - 1
の範囲のランダムが生成できました。
この値をchoices[~]
に入れると、choicesのランダムな要素を選び出すことができるというわけです。
選びだした結果をchosen
として覚えています。
// 結果のテキスト
let result = `今日のあなたの運勢は **{chosen}** です。`
前述のとおり、今日のあなたの運勢は**{chosen}**です。
の{chosen}
は具体的な値に置き換えられます。chosen
がギガ吉なら"今日のあなたの運勢は**ギガ吉**です。"
となります。
それをresultとして覚えます。
// UIを表示
Ui:render([
Ui:C:container({
align: 'center'
children: [
Ui:C:mfm({ text: result })
Ui:C:postFormButton({
text: "投稿する"
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
])
Ui:render()
はUIを作成する関数です。UIパーツの配列を入力することで、UIを表示することができます。
Ui:C:container()
はUIパーツをまとめるためのUIパーツを返す関数です。
Ui:C:mfm()
はMFMを表示するためのUIパーツを返す関数です。
Ui:C:postFormButton()
は投稿ボタンを表示するためのUIパーツを返す関数です。
UIパーツを返す関数にはオブジェクトと呼ばれるデータ構造のモノを入れます。オブジェクトは辞書オブジェクトと言われることもあり、モノとモノの詳細な特徴を記述するときに利用します。
オブジェクトの例
例えば、
{
apple: "red"
banana: "yellow"
}
みたいな感じです。
ちなみに、ここで言うapple
のことを「キー」、"red"
のことを「バリュー」と呼んだりもします。
また、オブジェクトは入れ子にしたり配列を入れたりすることもできます。
{
apple: {
color: [
{
red: 100
green: 0
blue: 0
}
{
red: 0
green: 100
blue: 0
}
]
}
grape: {
color: [
{
red: 100
green: 0
blue: 100
}
{
red: 0
green: 100
blue: 0
}
]
}
}
ちなみに、オブジェクトを変数で覚えると、
let fruits = {
apple: "red"
banana: "yellow"
}
fruits.apple // "red"
fruits["apple"] // "red"
のようにfruits.apple
やfruits["apple"]
も変数として覚えたみたいな感じになります。
また、オブジェクトも型の一種です。
Ui:C:container()
にはこんなオブジェクトを渡します。
{
align: 'center' // 文字揃え、'left'とかも可能
children: [
// 内側に入れるUIパーツいろいろ~~~~~
]
}
Ui:C:mfm()
にはこんなオブジェクトを渡します。
{
text: "表示したいMFM!!"
}
Ui:C:postFormButton()
にはこんなオブジェクトを渡します。
{
text: "投稿する" // ボタンに乗せるテキスト~~~
rounded: true // 角丸めるか丸めないか 丸めないならfalse
primary: true // 色つけるかつけないか つけないならfalse
form: {
text: "投稿するテキスト~~~~~"
}
}
こんなかんじ。
ちなみに
UIパーツには以下のようにIDをセットすることで、表示する文字を後から変更するような用途にも対応可能です。
Ui:C:mfm({ text: "abc" } "ididid")
Ui:get("ididid").update({text: "xyz"}) // こうやって変更する
長くなりましたが、この行では全要素中央揃えで、MFMでresult
として覚えた値を表示して、「投稿する」というテキストの色付き角丸投稿ボタンを表示して、投稿ボタンを押すと{result}{Str:lf}{THIS_URL}
で表されるテキストを投稿する画面になるように設定されています。(Str:lf
は何も言わなくても最初から改行が入っている変数で、THIS_URL
は何も言わなくても最初からこのPlayのURLが入っている変数です。)
Omikujiはこんな感じです。
簡単ですね。
Shuffle
巻き戻し機能がやや複雑なのでパス。
Quiz
/// @ 0.16.0
let title = '地理クイズ'
let qas = [{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
} {
q: '国土面積2番目の国は?'
choices: ['カナダ' 'アメリカ' '中国']
a: 'カナダ'
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
} {
q: '二重内陸国ではないのは?'
choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
a: 'レソト'
aDescription: 'レソトは(一重)内陸国です。'
} {
q: '閘門がない運河は?'
choices: ['キール運河' 'スエズ運河' 'パナマ運河']
a: 'スエズ運河'
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
}]
let qaEls = [Ui:C:container({
align: 'center'
children: [
Ui:C:text({
size: 1.5
bold: true
text: title
})
]
})]
var qn = 0
each (let qa, qas) {
qn += 1
qa.id = Util:uuid()
qaEls.push(Ui:C:container({
align: 'center'
bgColor: '#000'
fgColor: '#fff'
padding: 16
rounded: true
children: [
Ui:C:text({
text: `Q{qn} {qa.q}`
})
Ui:C:select({
items: qa.choices.map(@(c) {{ text: c, value: c }})
onChange: @(v) { qa.userAnswer = v }
})
Ui:C:container({
children: []
} `{qa.id}:a`)
]
} qa.id))
}
@finish() {
var score = 0
each (let qa, qas) {
let correct = qa.userAnswer == qa.a
if (correct) score += 1
let el = Ui:get(`{qa.id}:a`)
el.update({
children: [
Ui:C:text({
size: 1.2
bold: true
color: if (correct) '#f00' else '#00f'
text: if (correct) '🎉正解' else '不正解'
})
Ui:C:text({
text: qa.aDescription
})
]
})
}
let result = `{title}の結果は{qas.len}問中{score}問正解でした。`
Ui:get('footer').update({
children: [
Ui:C:postFormButton({
text: '結果を共有'
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
}
qaEls.push(Ui:C:container({
align: 'center'
children: [
Ui:C:button({
text: '答え合わせ'
primary: true
rounded: true
onClick: finish
})
]
} 'footer'))
Ui:render(qaEls)
let title = '地理クイズ'
「地理クイズ」という文字列をtitle
として覚えています。
let qas = [{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
} {
q: '国土面積2番目の国は?'
choices: ['カナダ' 'アメリカ' '中国']
a: 'カナダ'
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
} {
q: '二重内陸国ではないのは?'
choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
a: 'レソト'
aDescription: 'レソトは(一重)内陸国です。'
} {
q: '閘門がない運河は?'
choices: ['キール運河' 'スエズ運河' 'パナマ運河']
a: 'スエズ運河'
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
}]
質問と選択肢と解答と説明を「オブジェクトの配列」として表現し、qas
として覚えています。
let qaEls = [Ui:C:container({
align: 'center'
children: [
Ui:C:text({
size: 1.5
bold: true
text: title
})
]
})]
中央揃えにしてtitle
を表示するUIパーツの配列をqaEls
として覚えます。
var qn = 0
0
という数字をqn
として覚えています。let
ではなくvar
となっていますが、この2つの違いは覚えたものをあとから上書きできるかどうかです。上書きできない場合はlet
、上書きできる場合はvar
を使います。ただし、配列に値を追加したりオブジェクトにキーを追加したりする操作はlet
でも可能です。
each (let qa, qas) {
// ~~~
}
each (let qa, qas)
とは、「qas
配列から順番に要素を取り出して、それをqa
として覚えながら後ろの関数を繰り返す」という意味です。
例えば、
let words = ["しりとり" "りんご" "ゴリラ" "ラッパ" "パンダ"]
each (let word, words) {
say(word)
}
とすると、words
の内容を順にsay
関数に入力することになるというわけです。
qn += 1
qn
の内容に1を足すという意味です。each
の中なので、ループの中で順々にカウントアップされます。この変数は問題番号(Q1、Q2、...)を数えるのに使われます。
qa.id = Util:uuid()
qa
のid
キーをUtil:uuid()
関数の出力にするという意味です。id
キーは元々無いので、新しく作成されます。Util:uuid()
関数は、被る可能性の非常に低いランダム文字列を出力します。(40abde63-6c16-4fb1-a32c-a5efff44ccc2
←こんなかんじ) 被ることを心配するくらいなら隕石が落ちてくるのを心配したほうがいいほどなので、UUIDが被ることは考えなくてもいいです。
つまり、qa.id = Util:uuid()
をeachで全要素に行った場合、qasは以下のようになります。
[{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
+ id: "b0e31617-ad76-4a4b-bd8b-2ef83b084028"
} {
q: '国土面積2番目の国は?'
choices: ['カナダ' 'アメリカ' '中国']
a: 'カナダ'
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
+ id: "2e10b24b-38ff-476d-b08d-6836e403bdb2"
} {
q: '二重内陸国ではないのは?'
choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
a: 'レソト'
aDescription: 'レソトは(一重)内陸国です。'
+ id: "39008605-881b-4bd5-9ebc-1cde8ba4f006"
} {
q: '閘門がない運河は?'
choices: ['キール運河' 'スエズ運河' 'パナマ運河']
a: 'スエズ運河'
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
+ id: "09580b9f-2ba2-440f-8427-86f850211b44"
}]
qaEls.push(Ui:C:container({
align: 'center'
bgColor: '#000'
fgColor: '#fff'
padding: 16
rounded: true
children: [
Ui:C:text({
text: `Q{qn} {qa.q}`
})
Ui:C:select({
items: qa.choices.map(@(c) {{ text: c, value: c }})
onChange: @(v) { qa.userAnswer = v }
})
Ui:C:container({
children: []
} `{qa.id}:a`)
]
} qa.id))
qaEls.push()
関数は、qaEls
配列の最後に要素を追加します。追加する要素はカッコ内の入力で指定します。このコードでは、
Ui:C:container({
align: 'center'
bgColor: '#000'
fgColor: '#fff'
padding: 16
rounded: true
children: [
Ui:C:text({
text: `Q{qn} {qa.q}`
})
Ui:C:select({ // これはセレクトボックスのUIパーツ
items: qa.choices.map(@(c) {{ text: c, value: c }})
onChange: @(v) { qa.userAnswer = v }
})
Ui:C:container({
children: []
} `{qa.id}:a`)
]
} qa.id)
という要素を追加するようになっています。これは、全要素を中央揃えした黒背景・白文字・余白16・角丸のコンテナの中に「`Q{qn} {qa.q}`」という文字列(置き換えられて「Q(問題番号) (質問)」の形になる)、セレクトボックス、空のコンテナ(あとで正解/不正解を表示するのに使われるので、IDが`{qa.id}:a`として指定されています)があるようなUIパーツです。
セレクトボックスに渡しているオブジェクトのitems
キーには、qa.choices
の内容をmap()
関数で加工して渡しています。Ui:C:select()
の仕様に合うようにいじっているだけです。
map関数とは?
配列の要素それぞれに対して、入力で渡された関数を実行して帰ってくる値を集めてできる配列を返します(驚きのややこしさ)
たとえば、
let array = [0 1 2 3 4 5 6 7 8 9 10]
let array2 = array.map(@(v) {v * 10}) // [0 10 20 30 40 50 60 70 80 90 100]
という感じになります。
じゃあmap関数に渡しているその謎のアットマークはなんなんだと言う話ですが、これは関数を自分でその場で作るための記法です。上の例では「v
」として値を受け取って、v
の10倍の値を返す関数を作っています。
qa.choices.map(@(c) {{ text: c, value: c }})
この例では、qa.choices
配列に対してmap関数を実行することで、
['シドニー' 'キャンベラ' 'メルボルン']
これが
[
{ text: 'シドニー', value: 'シドニー' }
{ text: 'キャンベラ', value: 'キャンベラ' }
{ text: 'メルボルン', value: 'メルボルン' }
]
こうなります。
onChange
キーには、セレクトボックスの値が変化した際の処理が関数として書かれています。
@(v) { qa.userAnswer = v }
変化した後の選択肢を「v
」として受け取り、qa.userAnswer
をv
として設定する関数です。
userAnswer
というキーがない場合、新しく作成されます。
つまり、「オーストラリアの首都は?」という問いに「シドニー」と答えると、qas
の値はこうなります。
[{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
id: "b0e31617-ad76-4a4b-bd8b-2ef83b084028"
+ userAnswer: 'シドニー'
}
︙
「キャンベラ」と答え直すとこうです。
[{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
id: "b0e31617-ad76-4a4b-bd8b-2ef83b084028"
+ userAnswer: 'キャンベラ'
}
︙
@finish() {
var score = 0
each (let qa, qas) {
let correct = qa.userAnswer == qa.a
if (correct) score += 1
let el = Ui:get(`{qa.id}:a`)
el.update({
children: [
Ui:C:text({
size: 1.2
bold: true
color: if (correct) '#f00' else '#00f'
text: if (correct) '🎉正解' else '不正解'
})
Ui:C:text({
text: qa.aDescription
})
]
})
}
let result = `{title}の結果は{qas.len}問中{score}問正解でした。`
Ui:get('footer').update({
children: [
Ui:C:postFormButton({
text: '結果を共有'
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
}
答え合わせの処理やUI表示などをfinish()
関数として定義しています。細かい解説は一旦飛ばします。
qaEls.push(Ui:C:container({
align: 'center'
children: [
Ui:C:button({
text: '答え合わせ'
primary: true
rounded: true
onClick: finish
})
]
} 'footer'))
Ui:render(qaEls)
ここでは、qaEls
に答え合わせボタンを.push()
しています。あとから書き換え可能にするためにcontainerに'footer'
というIDを付与しています。答え合わせボタンをクリックしたときに、さっき定義したfinish()
関数を呼び出しています。
ここでもういちどfinish()
関数の中身を見てみます。
@finish() {
var score = 0
each (let qa, qas) {
let correct = qa.userAnswer == qa.a
if (correct) score += 1
let el = Ui:get(`{qa.id}:a`)
el.update({
children: [
Ui:C:text({
size: 1.2
bold: true
color: if (correct) '#f00' else '#00f'
text: if (correct) '🎉正解' else '不正解'
})
Ui:C:text({
text: qa.aDescription
})
]
})
}
let result = `{title}の結果は{qas.len}問中{score}問正解でした。`
Ui:get('footer').update({
children: [
Ui:C:postFormButton({
text: '結果を共有'
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
}
var score = 0
0
をscore
という名前で覚えています(上書き可能)
each (let qa, qas) {
let correct = qa.userAnswer == qa.a
if (correct) score += 1
let el = Ui:get(`{qa.id}:a`)
el.update({
children: [
Ui:C:text({
size: 1.2
bold: true
color: if (correct) '#f00' else '#00f'
text: if (correct) '🎉正解' else '不正解'
})
Ui:C:text({
text: qa.aDescription
})
]
})
}
ここでは、each
を使ってすべてのqas
配列の要素に対してループしています。
let correct = qa.userAnswer == qa.a
で、「qa.userAnswer
とqa.a
が等しい」かどうかを調べ、等しければtrue
、等しくなければfalse
という値をcorrect
として覚えます。
true, falseとは?
true, falseは型の1種で、「はい」か「いいえ」の2択の値を表現するためのものです。trueかfalseのことをbool型と言います。
「true」は「はい」、「false」は「いいえ」という意味です。
if
を使った処理で「もし変数がtrueなら」「もし変数がfalseなら」「2つの変数が両方trueなら」「2つの変数が両方falseなら」...みたいな処理を実行することができます。
let bool1 = true
let bool2 = false
if (bool1) { // bool1がtrueなら実行
say("bool1 is true!!")
}
if (!bool1) { // bool1がfalseなら実行(ビックリマークは否定を意味し、trueをfalseに、falseをtrueに変換する)
say("bool1 is false!!")
}
if (bool1 || bool2) { // bool1, bool2のどちらかがtrueなら実行
say("either bool1 or bool2 is true!!")
}
if (bool1 && bool2) { // bool1, bool2のどちらもtrueなら実行
say("bool1 and bool2 are true!!")
}
if (correct) score += 1
で、correct
がtrue
ならscore
を+1しています。
let el = Ui:get(`{qa.id}:a`)で、UIからIDが`{qa.id}:a`の要素を探して、見つかった要素をel
として覚えています。(正解か不正解かを表示するための空のcontainer
が見つかります)
el.update()
のくだりで正解か不正解かという文字列と解説に置き換えています。
let result = `{title}の結果は{qas.len}問中{score}問正解でした。`
Ui:get('footer').update({
children: [
Ui:C:postFormButton({
text: '結果を共有'
rounded: true
primary: true
form: {
text: `{result}{Str:lf}{THIS_URL}`
}
})
]
})
結果を説明する文字列をresult
として覚えて、idfooter
の要素に`{result}{Str:lf}{THIS_URL}`の投稿ボタンを追加しています。
Quizはこんなかんじ。
Timeline Viewer
書き疲れてきたのでふんわりあっさり解説。
/// @ 0.16.0
// APIリクエストを行いローカルタイムラインを表示するプリセット
@fetch() {
Ui:render([
Ui:C:container({
align: 'center'
children: [
Ui:C:text({ text: "読み込み中..." })
]
})
])
// タイムライン取得
let notes = Mk:api("notes/local-timeline" {})
// それぞれのノートごとにUI要素作成
let noteEls = []
each (let note, notes) {
// 表示名を設定していないアカウントはidを表示
let userName = if Core:type(note.user.name) == "str" note.user.name else note.user.username
// リノートもしくはメディア・投票のみで本文が無いノートに代替表示文を設定
let noteText = if Core:type(note.text) == "str" note.text else "(リノートもしくはメディア・投票のみのノート)"
let el = Ui:C:container({
bgColor: "#444"
fgColor: "#fff"
padding: 10
rounded: true
children: [
Ui:C:mfm({
text: userName
bold: true
})
Ui:C:mfm({
text: noteText
})
]
})
noteEls.push(el)
}
// UIを表示
Ui:render([
Ui:C:text({ text: "ローカル タイムライン" })
Ui:C:button({
text: "更新"
onClick: @() {
fetch()
}
})
Ui:C:container({
children: noteEls
})
])
}
fetch()
Mk:api()
でMisskeyのAPIを叩けます。APIの一覧はここにあります。
受け取った配列をeachでよしなに処理しています。
おわり
おわり
参考
Discussion