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 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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
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
+ [![CI](https://github.com/ydah/seimi/actions/workflows/ci.yml/badge.svg)](https://github.com/ydah/seimi/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/seimi.svg)](https://rubygems.org/gems/seimi)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-red.svg)](https://www.ruby-lang.org/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |task|
6
+ task.libs << "test"
7
+ task.pattern = "test/test_*.rb"
8
+ end
9
+
10
+ task default: :test
data/exe/seimi ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $stdout.set_encoding(Encoding::UTF_8)
5
+ $stderr.set_encoding(Encoding::UTF_8)
6
+
7
+ require "seimi/cli"
8
+
9
+ exit Seimi::CLI.run(ARGV)
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
@@ -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