seimi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +10 -0
- data/exe/seimi +9 -0
- data/lib/seimi/cli.rb +63 -0
- data/lib/seimi/elements.rb +124 -0
- data/lib/seimi/equation.rb +175 -0
- data/lib/seimi/errors.rb +15 -0
- data/lib/seimi/formula/parser.rb +361 -0
- data/lib/seimi/formula.rb +65 -0
- data/lib/seimi/isotopes.rb +1115 -0
- data/lib/seimi/kanji.rb +118 -0
- data/lib/seimi/sangi.rb +66 -0
- data/lib/seimi/version.rb +5 -0
- data/lib/seimi.rb +15 -0
- data/test/test_buntai.rb +26 -0
- data/test/test_cli.rb +25 -0
- data/test/test_equation.rb +73 -0
- data/test/test_formula.rb +78 -0
- data/test/test_helper.rb +6 -0
- data/test/test_kanji.rb +20 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 31c5e002df92b756bf33066db55078848852823d217b3c7bd1a1484fabd22496
|
|
4
|
+
data.tar.gz: e464f80f9c651d84e7a69eb3525ea7ac84b5965c22f15dfbd8dc67f2c817b2d9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 799935c2becb4d4f282c9948d8d57f83a621254f12c140161c10a5606e146f570f6da4bcae21bd0b2a78420505726b82a9377a0a3fbd539e8440e9051ffcc5bc
|
|
7
|
+
data.tar.gz: cbc2c1263ec400ed9f86d240e78fd5e64bcd7a71d74486f9f529acc9c0a8c9ec05492664b40b1590b7b028fe9a9ebc46f0e15968c189e1a22cbbd23b9db88119
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Seimi authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# seimi
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ydah/seimi/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/seimi)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](LICENSE.txt)
|
|
7
|
+
|
|
8
|
+
## 序
|
|
9
|
+
|
|
10
|
+
此gemは、化学式を解剖致し、分子の量を秤り、反応式の割合の数を自ら釣合はする計算絡繰に候。名は蘭学者宇田川榕菴先生の『舎密開宗』に肖り申候。
|
|
11
|
+
|
|
12
|
+
算用は今様のRubyにて執行ひ、貴殿の目に触るる詞は江戸後期の書付に寄せ候。式の読取り、元素の数上げ、割合の数の算出は標準の道具のみにて為し、実行の折に余分のgemを求めざる仕立に候。
|
|
13
|
+
|
|
14
|
+
## 誂方
|
|
15
|
+
|
|
16
|
+
左の命にて入れ給ふ可く候。
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gem install seimi
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
手許の写しより試し候折は、左の命を用ひ給ふ可く候。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle exec rake test
|
|
26
|
+
gem build seimi.gemspec
|
|
27
|
+
gem install ./seimi-0.1.0.gem
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 用方
|
|
31
|
+
|
|
32
|
+
書物の内にて用ひ候折は、先づ読込み給ふ可く候。
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "seimi"
|
|
36
|
+
|
|
37
|
+
Seimi.molar_mass("H2O")
|
|
38
|
+
#=> 18.015
|
|
39
|
+
|
|
40
|
+
formula = Seimi::Formula.parse("Ca(OH)2")
|
|
41
|
+
formula.composition
|
|
42
|
+
#=> {"Ca"=>1, "O"=>2, "H"=>2}
|
|
43
|
+
formula.molar_mass
|
|
44
|
+
#=> 74.092
|
|
45
|
+
|
|
46
|
+
equation = Seimi::Equation.balance("Fe + O2 -> Fe2O3")
|
|
47
|
+
equation.coefficients
|
|
48
|
+
#=> [4, 3, 2]
|
|
49
|
+
equation.to_s
|
|
50
|
+
#=> "4Fe + 3O2 -> 2Fe2O3"
|
|
51
|
+
|
|
52
|
+
ion = Seimi::Formula.parse("SO4^2-")
|
|
53
|
+
ion.charge
|
|
54
|
+
#=> -2
|
|
55
|
+
|
|
56
|
+
hydrate = Seimi::Formula.parse("CuSO4·5H2O")
|
|
57
|
+
hydrate.composition
|
|
58
|
+
#=> {"Cu"=>1, "S"=>1, "O"=>9, "H"=>10}
|
|
59
|
+
|
|
60
|
+
isotope = Seimi::Formula.parse("[13C]H4")
|
|
61
|
+
isotope.molar_mass
|
|
62
|
+
#=> 17.0353548351
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
命令の口より用ひ候折は、左の如く書付け給ふ可く候。
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
seimi kaibou "Ca(OH)2"
|
|
69
|
+
seimi tsuriai "Fe + O2 -> Fe2O3"
|
|
70
|
+
seimi tsuriai "Ag+ + Cl- -> AgCl"
|
|
71
|
+
seimi tsuriai "Ag++Cl-->AgCl"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
解剖の返書には、元素の内訳、分子の量、算木の図を掲げ候。
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
〔Ca(OH)2 解剖の覚〕
|
|
78
|
+
一、かるしうむ(Ca) 四十・〇七八 匁掛ける一つ
|
|
79
|
+
一、酸素(O) 三十一・九九八 匁掛ける二つ
|
|
80
|
+
一、水素(H) 二・〇一六 匁掛ける二つ
|
|
81
|
+
〆て 分子の量 凡そ七十四・〇九二 に候
|
|
82
|
+
右の如く相違なく候 也
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
釣合の返書には、元素釣合の盤と釣合ひたる式を掲げ候。
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
四Fe 三O2 を合せ、二Fe2O3 と成り申し候(洋数字にて 4Fe + 3O2 -> 2Fe2O3) 也
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
いおん式は `NH4+`、`Fe3+`、`SO4^2-`、`SO4²⁻` の形を受け候。反応式にていおんを用ひ候折は、`Ag+ + Cl-` の如く空白を置き候ても、`Ag++Cl-` の如く電荷の符と区切の符を続け候ても読取り候。
|
|
92
|
+
|
|
93
|
+
水和物は `CuSO4·5H2O` 又は `CuSO4.5H2O` の形を受け、点の後の数を水和の倍数として数へ候。同位体は `[13C]H4` 又は `^13CH4` の形を受け、分子の量には同梱の同位体質量表を用ひ、反応式の釣合にては同位体を別の印として扱ひ候。重水素 `D` と三重水素 `T` も水素同位体として読み候。
|
|
94
|
+
|
|
95
|
+
## 咎の品々
|
|
96
|
+
|
|
97
|
+
式の字に紛れ有る折は `Seimi::ParseError` を返し申候。元素表に無き記号は `Seimi::UnknownElementError` を返し申候。釣合の術無き反応式は `Seimi::UnbalancedError` を返し申候。
|
|
98
|
+
|
|
99
|
+
命令の絡繰は咎を捕へ、左の形にて知らせ申候。
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
咎: 見知らぬ元素に候: 「Xx」。当絡繰の知る元素は百十八種に候
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 名の由来
|
|
106
|
+
|
|
107
|
+
舎密は江戸後期に化学を指したる言葉に候。宇田川榕菴先生の『舎密開宗』は、西洋の化学を日本の学びへ移し入れたる書に候。酸素、水素、窒素、炭素等の訳語も、此学びの流れにて広まり候。此gemは其名を借り、今の計算を古き詞に包み候。
|
|
108
|
+
|
|
109
|
+
## 添状
|
|
110
|
+
|
|
111
|
+
此品はMITの札にて分け候。直し、試し、添書は何れも歓迎仕り候。然れども公開の前には、`bundle exec rake test` と `gem build seimi.gemspec` を通し給ふ可く候。
|
data/Rakefile
ADDED
data/exe/seimi
ADDED
data/lib/seimi/cli.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../seimi"
|
|
4
|
+
|
|
5
|
+
module Seimi
|
|
6
|
+
module CLI
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def run(argv)
|
|
10
|
+
command, *args = argv
|
|
11
|
+
|
|
12
|
+
case command
|
|
13
|
+
when nil, "tasuke"
|
|
14
|
+
puts usage
|
|
15
|
+
0
|
|
16
|
+
when "--version"
|
|
17
|
+
puts "seimi #{VERSION}"
|
|
18
|
+
0
|
|
19
|
+
when "kaibou"
|
|
20
|
+
return usage_error if args.empty?
|
|
21
|
+
|
|
22
|
+
formula = Formula.parse(args.join)
|
|
23
|
+
puts formula.to_kobun
|
|
24
|
+
0
|
|
25
|
+
when "tsuriai"
|
|
26
|
+
return usage_error if args.empty?
|
|
27
|
+
|
|
28
|
+
equation = Equation.balance(args.join(" "))
|
|
29
|
+
puts "〔反応式 釣合の覚〕"
|
|
30
|
+
puts matrix_text(equation)
|
|
31
|
+
puts equation.to_kobun
|
|
32
|
+
0
|
|
33
|
+
else
|
|
34
|
+
usage_error
|
|
35
|
+
end
|
|
36
|
+
rescue Seimi::Error => error
|
|
37
|
+
warn "咎: #{error.message}"
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def usage_error
|
|
42
|
+
puts usage
|
|
43
|
+
2
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def usage
|
|
47
|
+
<<~TEXT
|
|
48
|
+
用法: seimi kaibou <化学式> にて式を解剖し分子の量を量り申し候
|
|
49
|
+
用法: seimi tsuriai "<反応式>" にて割合の数を釣り合わせ申し候。反応式は引用符にて括り給うべく候
|
|
50
|
+
用法: seimi tasuke にて此の助けを開き給え
|
|
51
|
+
TEXT
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def matrix_text(equation)
|
|
55
|
+
lines = ["元素釣合の盤に候"]
|
|
56
|
+
equation.elements.each_with_index do |element, index|
|
|
57
|
+
row = equation.matrix[index].map { |value| Kanji.rational(value) }.join(" ")
|
|
58
|
+
lines << "#{element}: #{row}"
|
|
59
|
+
end
|
|
60
|
+
lines.join("\n")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seimi
|
|
4
|
+
ELEMENTS = {
|
|
5
|
+
"H" => [1.008, "水素"],
|
|
6
|
+
"He" => [4.0026, "ヘリウム"],
|
|
7
|
+
"Li" => [6.94, "リチウム"],
|
|
8
|
+
"Be" => [9.0122, "ベリリウム"],
|
|
9
|
+
"B" => [10.81, "硼素"],
|
|
10
|
+
"C" => [12.011, "炭素"],
|
|
11
|
+
"N" => [14.007, "窒素"],
|
|
12
|
+
"O" => [15.999, "酸素"],
|
|
13
|
+
"F" => [18.998, "弗素"],
|
|
14
|
+
"Ne" => [20.180, "ネオン"],
|
|
15
|
+
"Na" => [22.990, "ナトリウム"],
|
|
16
|
+
"Mg" => [24.305, "マグネシウム"],
|
|
17
|
+
"Al" => [26.982, "アルミニウム"],
|
|
18
|
+
"Si" => [28.085, "珪素"],
|
|
19
|
+
"P" => [30.974, "燐"],
|
|
20
|
+
"S" => [32.06, "硫黄"],
|
|
21
|
+
"Cl" => [35.45, "塩素"],
|
|
22
|
+
"Ar" => [39.948, "アルゴン"],
|
|
23
|
+
"K" => [39.098, "カリウム"],
|
|
24
|
+
"Ca" => [40.078, "カルシウム"],
|
|
25
|
+
"Sc" => [44.956, "スカンジウム"],
|
|
26
|
+
"Ti" => [47.867, "チタン"],
|
|
27
|
+
"V" => [50.942, "バナジウム"],
|
|
28
|
+
"Cr" => [51.996, "クロム"],
|
|
29
|
+
"Mn" => [54.938, "マンガン"],
|
|
30
|
+
"Fe" => [55.845, "鉄"],
|
|
31
|
+
"Co" => [58.933, "コバルト"],
|
|
32
|
+
"Ni" => [58.693, "ニッケル"],
|
|
33
|
+
"Cu" => [63.546, "銅"],
|
|
34
|
+
"Zn" => [65.38, "亜鉛"],
|
|
35
|
+
"Ga" => [69.723, "ガリウム"],
|
|
36
|
+
"Ge" => [72.630, "ゲルマニウム"],
|
|
37
|
+
"As" => [74.922, "砒素"],
|
|
38
|
+
"Se" => [78.971, "セレン"],
|
|
39
|
+
"Br" => [79.904, "臭素"],
|
|
40
|
+
"Kr" => [83.798, "クリプトン"],
|
|
41
|
+
"Rb" => [85.468, "ルビジウム"],
|
|
42
|
+
"Sr" => [87.62, "ストロンチウム"],
|
|
43
|
+
"Y" => [88.906, "イットリウム"],
|
|
44
|
+
"Zr" => [91.224, "ジルコニウム"],
|
|
45
|
+
"Nb" => [92.906, "ニオブ"],
|
|
46
|
+
"Mo" => [95.95, "モリブデン"],
|
|
47
|
+
"Tc" => [98.0, "テクネチウム"],
|
|
48
|
+
"Ru" => [101.07, "ルテニウム"],
|
|
49
|
+
"Rh" => [102.91, "ロジウム"],
|
|
50
|
+
"Pd" => [106.42, "パラジウム"],
|
|
51
|
+
"Ag" => [107.87, "銀"],
|
|
52
|
+
"Cd" => [112.41, "カドミウム"],
|
|
53
|
+
"In" => [114.82, "インジウム"],
|
|
54
|
+
"Sn" => [118.71, "錫"],
|
|
55
|
+
"Sb" => [121.76, "アンチモン"],
|
|
56
|
+
"Te" => [127.60, "テルル"],
|
|
57
|
+
"I" => [126.90, "沃素"],
|
|
58
|
+
"Xe" => [131.29, "キセノン"],
|
|
59
|
+
"Cs" => [132.91, "セシウム"],
|
|
60
|
+
"Ba" => [137.33, "バリウム"],
|
|
61
|
+
"La" => [138.91, "ランタン"],
|
|
62
|
+
"Ce" => [140.12, "セリウム"],
|
|
63
|
+
"Pr" => [140.91, "プラセオジム"],
|
|
64
|
+
"Nd" => [144.24, "ネオジム"],
|
|
65
|
+
"Pm" => [145.0, "プロメチウム"],
|
|
66
|
+
"Sm" => [150.36, "サマリウム"],
|
|
67
|
+
"Eu" => [151.96, "ユウロピウム"],
|
|
68
|
+
"Gd" => [157.25, "ガドリニウム"],
|
|
69
|
+
"Tb" => [158.93, "テルビウム"],
|
|
70
|
+
"Dy" => [162.50, "ジスプロシウム"],
|
|
71
|
+
"Ho" => [164.93, "ホルミウム"],
|
|
72
|
+
"Er" => [167.26, "エルビウム"],
|
|
73
|
+
"Tm" => [168.93, "ツリウム"],
|
|
74
|
+
"Yb" => [173.05, "イッテルビウム"],
|
|
75
|
+
"Lu" => [174.97, "ルテチウム"],
|
|
76
|
+
"Hf" => [178.49, "ハフニウム"],
|
|
77
|
+
"Ta" => [180.95, "タンタル"],
|
|
78
|
+
"W" => [183.84, "タングステン"],
|
|
79
|
+
"Re" => [186.21, "レニウム"],
|
|
80
|
+
"Os" => [190.23, "オスミウム"],
|
|
81
|
+
"Ir" => [192.22, "イリジウム"],
|
|
82
|
+
"Pt" => [195.08, "白金"],
|
|
83
|
+
"Au" => [196.97, "金"],
|
|
84
|
+
"Hg" => [200.59, "水銀"],
|
|
85
|
+
"Tl" => [204.38, "タリウム"],
|
|
86
|
+
"Pb" => [207.2, "鉛"],
|
|
87
|
+
"Bi" => [208.98, "蒼鉛"],
|
|
88
|
+
"Po" => [209.0, "ポロニウム"],
|
|
89
|
+
"At" => [210.0, "アスタチン"],
|
|
90
|
+
"Rn" => [222.0, "ラドン"],
|
|
91
|
+
"Fr" => [223.0, "フランシウム"],
|
|
92
|
+
"Ra" => [226.0, "ラジウム"],
|
|
93
|
+
"Ac" => [227.0, "アクチニウム"],
|
|
94
|
+
"Th" => [232.04, "トリウム"],
|
|
95
|
+
"Pa" => [231.04, "プロトアクチニウム"],
|
|
96
|
+
"U" => [238.03, "ウラン"],
|
|
97
|
+
"Np" => [237.0, "ネプツニウム"],
|
|
98
|
+
"Pu" => [244.0, "プルトニウム"],
|
|
99
|
+
"Am" => [243.0, "アメリシウム"],
|
|
100
|
+
"Cm" => [247.0, "キュリウム"],
|
|
101
|
+
"Bk" => [247.0, "バークリウム"],
|
|
102
|
+
"Cf" => [251.0, "カリホルニウム"],
|
|
103
|
+
"Es" => [252.0, "アインスタイニウム"],
|
|
104
|
+
"Fm" => [257.0, "フェルミウム"],
|
|
105
|
+
"Md" => [258.0, "メンデレビウム"],
|
|
106
|
+
"No" => [259.0, "ノーベリウム"],
|
|
107
|
+
"Lr" => [266.0, "ローレンシウム"],
|
|
108
|
+
"Rf" => [267.0, "ラザホージウム"],
|
|
109
|
+
"Db" => [268.0, "ドブニウム"],
|
|
110
|
+
"Sg" => [269.0, "シーボーギウム"],
|
|
111
|
+
"Bh" => [270.0, "ボーリウム"],
|
|
112
|
+
"Hs" => [277.0, "ハッシウム"],
|
|
113
|
+
"Mt" => [278.0, "マイトネリウム"],
|
|
114
|
+
"Ds" => [281.0, "ダームスタチウム"],
|
|
115
|
+
"Rg" => [282.0, "レントゲニウム"],
|
|
116
|
+
"Cn" => [285.0, "コペルニシウム"],
|
|
117
|
+
"Nh" => [286.0, "ニホニウム"],
|
|
118
|
+
"Fl" => [289.0, "フレロビウム"],
|
|
119
|
+
"Mc" => [290.0, "モスコビウム"],
|
|
120
|
+
"Lv" => [293.0, "リバモリウム"],
|
|
121
|
+
"Ts" => [294.0, "テネシン"],
|
|
122
|
+
"Og" => [294.0, "オガネソン"]
|
|
123
|
+
}.transform_values(&:freeze).freeze
|
|
124
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "formula"
|
|
5
|
+
|
|
6
|
+
module Seimi
|
|
7
|
+
class Equation
|
|
8
|
+
CHARGE_BALANCE_SYMBOL = "電荷"
|
|
9
|
+
|
|
10
|
+
Result = Struct.new(:coefficients, :species, :lhs_size, :matrix, :elements, keyword_init: true) do
|
|
11
|
+
def to_s
|
|
12
|
+
"#{format_side(0...lhs_size)} -> #{format_side(lhs_size...species.length)}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_kobun
|
|
16
|
+
require_relative "kanji"
|
|
17
|
+
|
|
18
|
+
left = kobun_terms(0...lhs_size).join(" ")
|
|
19
|
+
right = kobun_terms(lhs_size...species.length).join(" ")
|
|
20
|
+
"#{left} を合せ、#{right} と成り申し候(洋数字にて #{self}) 也"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def format_side(range)
|
|
26
|
+
range.map { |index| format_term(coefficients[index], species[index]) }.join(" + ")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_term(coefficient, formula)
|
|
30
|
+
coefficient == 1 ? formula : "#{coefficient}#{formula}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def kobun_terms(range)
|
|
34
|
+
range.map do |index|
|
|
35
|
+
coefficient = coefficients[index]
|
|
36
|
+
coefficient == 1 ? species[index] : "#{Kanji.from_i(coefficient)}#{species[index]}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def balance(source)
|
|
43
|
+
lhs, rhs = split_equation(source)
|
|
44
|
+
lhs_species = parse_species(lhs)
|
|
45
|
+
rhs_species = parse_species(rhs)
|
|
46
|
+
species = lhs_species + rhs_species
|
|
47
|
+
lhs_size = lhs_species.length
|
|
48
|
+
formulas = species.map { |item| Formula.parse(item) }
|
|
49
|
+
elements, matrix = build_matrix(formulas, lhs_size)
|
|
50
|
+
coefficients = nullspace_coefficients(matrix)
|
|
51
|
+
|
|
52
|
+
Result.new(
|
|
53
|
+
coefficients: coefficients,
|
|
54
|
+
species: species,
|
|
55
|
+
lhs_size: lhs_size,
|
|
56
|
+
matrix: matrix.map(&:dup),
|
|
57
|
+
elements: elements
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def split_equation(source)
|
|
64
|
+
parts = String(source).split(/->|→|=/, -1)
|
|
65
|
+
raise ParseError, MSG_EQUATION_DELIMITER unless parts.length == 2
|
|
66
|
+
|
|
67
|
+
lhs = parts[0].strip
|
|
68
|
+
rhs = parts[1].strip
|
|
69
|
+
raise ParseError, MSG_EQUATION_DELIMITER if lhs.empty? || rhs.empty?
|
|
70
|
+
|
|
71
|
+
[lhs, rhs]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_species(side)
|
|
75
|
+
separator = /\s+\+\s+/
|
|
76
|
+
raw_items = if side.match?(separator)
|
|
77
|
+
side.split(separator)
|
|
78
|
+
else
|
|
79
|
+
side.split(/(?<=[A-Za-z0-9)\]⁺⁻+\-])\+(?=\d*[A-Z(\[\^])/)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
items = raw_items.map do |item|
|
|
83
|
+
item.strip.sub(/\A\d+\s*/, "")
|
|
84
|
+
end
|
|
85
|
+
raise ParseError, MSG_EQUATION_DELIMITER if items.any?(&:empty?)
|
|
86
|
+
|
|
87
|
+
items
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_matrix(formulas, lhs_size)
|
|
91
|
+
elements = []
|
|
92
|
+
formulas.each do |formula|
|
|
93
|
+
formula.balance_composition.each_key do |symbol|
|
|
94
|
+
elements << symbol unless elements.include?(symbol)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
elements << CHARGE_BALANCE_SYMBOL if formulas.any?(&:charged?)
|
|
98
|
+
|
|
99
|
+
matrix = elements.map do |symbol|
|
|
100
|
+
formulas.each_with_index.map do |formula, index|
|
|
101
|
+
sign = index < lhs_size ? 1 : -1
|
|
102
|
+
count = symbol == CHARGE_BALANCE_SYMBOL ? formula.charge : formula.balance_composition.fetch(symbol, 0)
|
|
103
|
+
Rational(sign * count, 1)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[elements, matrix]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def nullspace_coefficients(matrix)
|
|
111
|
+
rref_matrix, pivot_columns = rref(matrix)
|
|
112
|
+
column_count = matrix.first&.length || 0
|
|
113
|
+
free_columns = (0...column_count).to_a - pivot_columns
|
|
114
|
+
raise UnbalancedError, MSG_UNBALANCED_NO_SOLUTION if free_columns.empty?
|
|
115
|
+
|
|
116
|
+
vector = Array.new(column_count, Rational(0))
|
|
117
|
+
free_columns.each { |column| vector[column] = Rational(1) }
|
|
118
|
+
|
|
119
|
+
pivot_columns.each_with_index.reverse_each do |pivot_column, row|
|
|
120
|
+
sum = free_columns.sum(Rational(0)) do |free_column|
|
|
121
|
+
rref_matrix[row][free_column] * vector[free_column]
|
|
122
|
+
end
|
|
123
|
+
vector[pivot_column] = -sum
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
integers = rational_vector_to_integers(vector)
|
|
127
|
+
first = integers.find { |value| !value.zero? }
|
|
128
|
+
integers.map! { |value| -value } if first&.negative?
|
|
129
|
+
raise UnbalancedError, MSG_UNBALANCED_NON_POSITIVE if integers.any? { |value| value <= 0 }
|
|
130
|
+
|
|
131
|
+
divisor = integers.map(&:abs).reduce(0) { |gcd, value| gcd.gcd(value) }
|
|
132
|
+
integers.map { |value| value / divisor }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def rref(matrix)
|
|
136
|
+
work = matrix.map(&:dup)
|
|
137
|
+
row_count = work.length
|
|
138
|
+
column_count = work.first&.length || 0
|
|
139
|
+
pivot_columns = []
|
|
140
|
+
row = 0
|
|
141
|
+
|
|
142
|
+
(0...column_count).each do |column|
|
|
143
|
+
pivot_row = (row...row_count).find { |candidate| !work[candidate][column].zero? }
|
|
144
|
+
next unless pivot_row
|
|
145
|
+
|
|
146
|
+
work[row], work[pivot_row] = work[pivot_row], work[row] unless row == pivot_row
|
|
147
|
+
pivot = work[row][column]
|
|
148
|
+
(0...column_count).each { |index| work[row][index] /= pivot }
|
|
149
|
+
|
|
150
|
+
(0...row_count).each do |other_row|
|
|
151
|
+
next if other_row == row
|
|
152
|
+
|
|
153
|
+
factor = work[other_row][column]
|
|
154
|
+
next if factor.zero?
|
|
155
|
+
|
|
156
|
+
(0...column_count).each do |index|
|
|
157
|
+
work[other_row][index] -= factor * work[row][index]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
pivot_columns << column
|
|
162
|
+
row += 1
|
|
163
|
+
break if row == row_count
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
[work, pivot_columns]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def rational_vector_to_integers(vector)
|
|
170
|
+
lcm = vector.map(&:denominator).reduce(1) { |multiple, denominator| multiple.lcm(denominator) }
|
|
171
|
+
vector.map { |value| (value * lcm).to_i }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
data/lib/seimi/errors.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seimi
|
|
4
|
+
MSG_UNKNOWN_ELEMENT = "見知らぬ元素に候: 「%s」。当絡繰の知る元素は百十八種に候"
|
|
5
|
+
MSG_PARSE_INVALID_CHAR = "化学式に置き難き字に候: 「%s」"
|
|
6
|
+
MSG_UNCLOSED_PAREN = "括弧、閉じざるままに候。今一度式を改め給え"
|
|
7
|
+
MSG_EQUATION_DELIMITER = "反応式は「->」にて左右に分かち給うべく候"
|
|
8
|
+
MSG_UNBALANCED_NO_SOLUTION = "この式、釣り合いの術なく候。元素の出入りを確かめ給え"
|
|
9
|
+
MSG_UNBALANCED_NON_POSITIVE = "割合の数、乱れたり。式に紛れあるやに候"
|
|
10
|
+
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class ParseError < Error; end
|
|
13
|
+
class UnknownElementError < Error; end
|
|
14
|
+
class UnbalancedError < Error; end
|
|
15
|
+
end
|