raheui 1.0.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.
@@ -0,0 +1,212 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Run Aheui code.
4
+ class Runner
5
+ # Point class for execution cursor position and delta of it.
6
+ Point = Struct.new(:x, :y)
7
+
8
+ # Numbers of required elements for each command.
9
+ REQUIRED_STORE_SIZE = [
10
+ 0, 0, 2, 2, 2, 2, 1, 0, 1, 0, # ㄱ ㄲ ㄴ ㄷ ㄸ ㄹ ㅁ ㅂ ㅃ ㅅ
11
+ 1, 0, 2, 0, 1, 0, 2, 2, 0 # ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ
12
+ ]
13
+
14
+ # Delta values of each medial consonant.
15
+ MEDIAL_DELTAS = [
16
+ [1, 0], nil, [2, 0], nil, # ㅏ ㅐ ㅑ ㅒ
17
+ [-1, 0], nil, [-2, 0], nil, # ㅓ ㅔ ㅕ ㅖ
18
+ [0, -1], nil, nil, nil, [0, -2], # ㅗ ㅘ ㅙ ㅚ ㅛ
19
+ [0, 1], nil, nil, nil, [0, 2], # ㅜ ㅝ ㅞ ㅟ ㅠ
20
+ [:+, :-], [:-, :-], [:-, :+] # ㅡ ㅢ ㅣ
21
+ ]
22
+
23
+ # Numbers of strokes of each final consonant.
24
+ FINAL_STROKES = [
25
+ 0, # No final consonant.
26
+ 2, 4, 4, 2, 5, 5, 3, 5, 7, 9, # ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄹ ㄺ ㄻ
27
+ 9, 7, 9, 9, 8, 4, 4, 6, 2, 4, # ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅄ ㅅ ㅆ
28
+ 1, 3, 4, 3, 4, 4, 3 # ㅇ ㅈ ㅊ ㅋ ㅌ ㅍ ㅎ
29
+ ]
30
+
31
+ private_constant :REQUIRED_STORE_SIZE, :MEDIAL_DELTAS, :FINAL_STROKES
32
+
33
+ # Initialize a Runner. Get a Code instance and initialize Stores.
34
+ #
35
+ # code - The Code instance to execute.
36
+ def initialize(code)
37
+ @code = code
38
+ end
39
+
40
+ # Run the Aheui Code.
41
+ #
42
+ # Returns the Integer exit code.
43
+ def run
44
+ reset
45
+ step until @finished
46
+ @selected_store.pop || 0
47
+ end
48
+
49
+ private
50
+
51
+ # Reset cursor and Stores. Select first Store.
52
+ #
53
+ # Returns nothing.
54
+ def reset
55
+ @cursor = Point.new(0, 0)
56
+ @delta = Point.new(0, 1)
57
+ @stores = Code::FINAL_CONSONANTS.times.map do |consonant|
58
+ case consonant
59
+ when 21 then Queue.new # ㅇ
60
+ when 27 then Port.new # ㅎ
61
+ else Stack.new
62
+ end
63
+ end
64
+ @selected_store = @stores[0]
65
+ @finished = false
66
+ end
67
+
68
+ # Process current character which cursor points to and move cursor.
69
+ #
70
+ # Returns nothing.
71
+ def step
72
+ consonants = @code[@cursor.x, @cursor.y]
73
+ unless consonants.empty?
74
+ initial, medial, final = consonants
75
+ @delta.x, @delta.y = delta(medial)
76
+ if @selected_store.size < REQUIRED_STORE_SIZE[initial]
77
+ @delta.x, @delta.y = -@delta.x, -@delta.y
78
+ else
79
+ process(initial, final)
80
+ end
81
+ end
82
+ move
83
+ end
84
+
85
+ # Process a Korean alphabet.
86
+ #
87
+ # initial - An Integer index of initial consonant of the Korean alphabet.
88
+ # final - An Integer index of final consonant of the Korean alphabet.
89
+ #
90
+ # Returns nothing.
91
+ def process(initial, final)
92
+ case initial
93
+ when 2 # ㄴ
94
+ operate(:/)
95
+ when 3 # ㄷ
96
+ operate(:+)
97
+ when 4 # ㄸ
98
+ operate(:*)
99
+ when 5 # ㄹ
100
+ operate(:%)
101
+ when 6 # ㅁ
102
+ op = @selected_store.pop
103
+ if final == 21 # ㅇ
104
+ IO.print_int(op)
105
+ elsif final == 27 # ㅎ
106
+ IO.print_chr(op)
107
+ end
108
+ when 7 # ㅂ
109
+ op = if final == 21 # ㅇ
110
+ IO.read_int
111
+ elsif final == 27 # ㅎ
112
+ IO.read_chr
113
+ else
114
+ FINAL_STROKES[final]
115
+ end
116
+ @selected_store.push(op)
117
+ when 8 # ㅃ
118
+ @selected_store.push_dup
119
+ when 9 # ㅅ
120
+ @selected_store = @stores[final]
121
+ when 10 # ㅆ
122
+ op = @selected_store.pop
123
+ @stores[final].push(op)
124
+ when 12 # ㅈ
125
+ op1 = @selected_store.pop
126
+ op2 = @selected_store.pop
127
+ @selected_store.push(op2 >= op1 ? 1 : 0)
128
+ when 14 # ㅊ
129
+ op = @selected_store.pop
130
+ @delta.x, @delta.y = -@delta.x, -@delta.y if op == 0
131
+ when 16 # ㅌ
132
+ operate(:-)
133
+ when 17 # ㅍ
134
+ @selected_store.swap
135
+ when 18 # ㅎ
136
+ @finished = true
137
+ end
138
+ end
139
+
140
+ # Helper method for basic operators. +, -, *, / and % can be processed.
141
+ #
142
+ # method - A Symbol method to execute.
143
+ #
144
+ # Examples
145
+ #
146
+ # operate(:+)
147
+ #
148
+ # operate(:-)
149
+ #
150
+ # operate(:*)
151
+ #
152
+ # operate(:/)
153
+ #
154
+ # operate(:%)
155
+ #
156
+ # Returns nothing.
157
+ def operate(method)
158
+ op1 = @selected_store.pop
159
+ op2 = @selected_store.pop
160
+ @selected_store.push([op2, op1].reduce(method))
161
+ end
162
+
163
+ # Get delta x and y position for next move.
164
+ #
165
+ # medial - An Integer index of medial consonant of the Korean alphabet.
166
+ #
167
+ # Examples
168
+ #
169
+ # delta(0)
170
+ # # => [1, 0]
171
+ #
172
+ # Returns an Array of Integer delta x and y position.
173
+ def delta(medial)
174
+ delta = MEDIAL_DELTAS[medial]
175
+ if delta
176
+ x, y = delta
177
+ x = x == :+ ? @delta.x : -@delta.x if x.is_a? Symbol
178
+ y = y == :+ ? @delta.y : -@delta.y if y.is_a? Symbol
179
+ [x, y]
180
+ else
181
+ [@delta.x, @delta.y]
182
+ end
183
+ end
184
+
185
+ # Move cursor to proper position. Wrap the position if it goes to outside of
186
+ # the code.
187
+ #
188
+ # Returns nothing.
189
+ def move
190
+ @cursor.x = wrap(@cursor.x + @delta.x, @code.width)
191
+ @cursor.y = wrap(@cursor.y + @delta.y, @code.height)
192
+ end
193
+
194
+ # Wrap a number to be between 0 and max value excluding max value. If the
195
+ # number is negative, it goes to max - 1. If the number is bigger than or
196
+ # equal to max value, it goes to zero.
197
+ #
198
+ # num - An Integer to be wrapped.
199
+ # max - An Integer max value.
200
+ #
201
+ # Returns an Integer between 0 and max - 1.
202
+ def wrap(num, max)
203
+ if num < 0
204
+ max - 1
205
+ elsif num >= max
206
+ 0
207
+ else
208
+ num
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Stack class for Aheui.
4
+ class Stack < Store
5
+ # Delegates push, pop to @store.
6
+ delegate [:push, :pop] => :@store
7
+
8
+ # Swap the last two elements of Stack.
9
+ def swap
10
+ @store[-1], @store[-2] = @store[-2], @store[-1] if size > 1
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Base Store class for Aheui. Every child classes should implement push, pop
4
+ # and swap method.
5
+ class Store
6
+ extend Forwardable
7
+
8
+ BASE_METHODS = [:push, :pop, :swap]
9
+ private_constant :BASE_METHODS
10
+
11
+ # Delegates size to @store.
12
+ def_delegator :@store, :size
13
+
14
+ # Initialize a Stack.
15
+ def initialize
16
+ check_base_methods
17
+ @store = []
18
+ end
19
+
20
+ # Push the last element to Store.
21
+ def push_dup
22
+ push(@store.last) if size > 0
23
+ end
24
+
25
+ private
26
+
27
+ # Check whether base methods are implemented.
28
+ #
29
+ # Returns nothing.
30
+ # Raises NotImplementedError if base methods are not implemented.
31
+ def check_base_methods
32
+ errors = []
33
+ BASE_METHODS.each do |method|
34
+ errors << method unless respond_to?(method)
35
+ end
36
+ return if errors.empty?
37
+ fail NotImplementedError, 'base methods are not implemented:' \
38
+ " #{errors.join(', ')}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ module Raheui
3
+ # Holds the Raheui version information.
4
+ module Version
5
+ STRING = '1.0.0'
6
+ end
7
+ end
data/lib/raheui.rb ADDED
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require 'raheui/version'
3
+
4
+ require 'forwardable'
5
+ require 'raheui/store'
6
+ require 'raheui/stack'
7
+ require 'raheui/queue'
8
+ require 'raheui/port'
9
+
10
+ require 'raheui/io'
11
+ require 'raheui/code'
12
+ require 'raheui/runner'
13
+
14
+ require 'optparse'
15
+ require 'raheui/option'
16
+ require 'raheui/cli'
data/raheui.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'raheui/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'raheui'
8
+ spec.version = Raheui::Version::STRING
9
+ spec.authors = ['ChaYoung You']
10
+ spec.email = ['yousbe@gmail.com']
11
+ spec.summary = 'Aheui interpreter in Ruby.'
12
+ spec.description = 'Aheui interpreter in Ruby.'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ spec.add_development_dependency 'rubocop', '~> 0.25.0'
25
+ spec.add_development_dependency 'simplecov', '~> 0.9'
26
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::CLI do
5
+ include FileHelper
6
+ include_context 'isolated environment'
7
+
8
+ subject(:cli) { described_class.new }
9
+
10
+ before(:example) { $stdout = StringIO.new }
11
+ after(:example) { $stdout = STDOUT }
12
+
13
+ it 'runs aheui file passed as an argument' do
14
+ create_file('helloworld.aheui', %w(밤밣따빠밣밟따뿌
15
+ 빠맣파빨받밤뚜뭏
16
+ 돋밬탕빠맣붏두붇
17
+ 볻뫃박발뚷투뭏붖
18
+ 뫃도뫃희멓뭏뭏붘
19
+ 뫃봌토범더벌뿌뚜
20
+ 뽑뽀멓멓더벓뻐뚠
21
+ 뽀덩벐멓뻐덕더벅))
22
+ expect(cli.run(['helloworld.aheui'])).to be(0)
23
+ expect($stdout.string).to eq("Hello, world!\n")
24
+ end
25
+
26
+ it 'runs string passed as an stdin argument' do
27
+ allow($stdin).to receive(:read).once
28
+ .and_return(%w(밤밣따빠밣밟따뿌
29
+ 빠맣파빨받밤뚜뭏
30
+ 돋밬탕빠맣붏두붇
31
+ 볻뫃박발뚷투뭏붖
32
+ 뫃도뫃희멓뭏뭏붘
33
+ 뫃봌토범더벌뿌뚜
34
+ 뽑뽀멓멓더벓뻐뚠
35
+ 뽀덩벐멓뻐덕더벅).join("\n"))
36
+ expect(cli.run([])).to be(0)
37
+ expect($stdout.string).to eq("Hello, world!\n")
38
+ end
39
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Code do
5
+ shared_examples 'a code' do
6
+ subject(:code) { Raheui::Code.new(code_str) }
7
+ let(:consonants) { code.method(:consonants) }
8
+
9
+ it 'has width which is maximum line length of code with minimum value 1' do
10
+ width = code_str.lines.map { |line| line.chomp.size }.max || 1
11
+ expect(code.width).to be(width)
12
+ end
13
+
14
+ it 'has height which is number of lines with minimum value 1' do
15
+ height = [code_str.lines.count, 1].max
16
+ expect(code.height).to be(height)
17
+ end
18
+
19
+ describe '#[]' do
20
+ it 'returns item at given position' do
21
+ code_str.lines.each_with_index do |line, y|
22
+ line.chomp.chars.each_with_index do |ch, x|
23
+ expect(code[x, y]).to match_array(consonants.call(ch))
24
+ end
25
+ end
26
+ end
27
+
28
+ it 'returns item at given negative position' do
29
+ code_str.lines.each_with_index do |line, y|
30
+ max_x = line.chomp.chars.count
31
+ line.chomp.chars.each_with_index do |ch, x|
32
+ expected = consonants.call(ch)
33
+ expect(code[x - max_x, y]).to match_array(expected)
34
+ expect(code[x, y - code.height]).to match_array(expected)
35
+ expect(code[x - max_x, y - code.height]).to match_array(expected)
36
+ end
37
+ end
38
+ end
39
+
40
+ it 'returns an empty array if there is no item at given position' do
41
+ expect(code[0, code.height]).to match_array([])
42
+ expect(code[code.width, 0]).to match_array([])
43
+ expect(code[code.width, code.height]).to match_array([])
44
+ end
45
+ end
46
+ end
47
+
48
+ describe Raheui::Code, 'with empty code' do
49
+ let(:code_str) { '' }
50
+ it_behaves_like 'a code'
51
+ end
52
+
53
+ describe Raheui::Code, 'with aheui code' do
54
+ let(:code_str) { '아희' }
55
+ it_behaves_like 'a code'
56
+ end
57
+
58
+ describe Raheui::Code, 'with hello world code' do
59
+ let(:code_str) do
60
+ <<-CODE
61
+ 밤밣따빠밣밟따뿌
62
+ 빠맣파빨받밤뚜뭏
63
+ 돋밬탕빠맣붏두붇
64
+ 볻뫃박발뚷투뭏붖
65
+ 뫃도뫃희멓뭏뭏붘
66
+ 뫃봌토범더벌뿌뚜
67
+ 뽑뽀멓멓더벓뻐뚠
68
+ 뽀덩벐멓뻐덕더벅
69
+ CODE
70
+ end
71
+
72
+ it_behaves_like 'a code'
73
+ end
74
+
75
+ describe '#consonants' do
76
+ subject { Raheui::Code.new('') }
77
+ let(:consonants) { subject.method(:consonants) }
78
+ before(:example) do
79
+ stub_const('INITIAL_CONSONANTS', Raheui::Code::INITIAL_CONSONANTS)
80
+ stub_const('MEDIAL_CONSONANTS', Raheui::Code::MEDIAL_CONSONANTS)
81
+ stub_const('FINAL_CONSONANTS', Raheui::Code::FINAL_CONSONANTS)
82
+ end
83
+
84
+ it 'returns consonants of Korean alphabet' do
85
+ examples = [
86
+ # 가
87
+ [0, 0, 0],
88
+ # 힣
89
+ [INITIAL_CONSONANTS - 1, MEDIAL_CONSONANTS - 1, FINAL_CONSONANTS - 1]
90
+ ]
91
+ examples.concat(10.times.map do
92
+ [rand(INITIAL_CONSONANTS),
93
+ rand(MEDIAL_CONSONANTS),
94
+ rand(FINAL_CONSONANTS)]
95
+ end.uniq)
96
+ examples.each do |initial, medial, final|
97
+ alphabet = (
98
+ 0xAC00 +
99
+ initial * MEDIAL_CONSONANTS * FINAL_CONSONANTS +
100
+ medial * FINAL_CONSONANTS +
101
+ final
102
+ ).chr(Encoding::UTF_8)
103
+ expect(consonants.call(alphabet)).to match_array(
104
+ [initial, medial, final])
105
+ end
106
+ end
107
+
108
+ it 'returns an empty array for non-Korean alphabet' do
109
+ examples = [
110
+ *0..127,
111
+ 0xAC00 + INITIAL_CONSONANTS * MEDIAL_CONSONANTS * FINAL_CONSONANTS
112
+ ]
113
+ examples.concat(%w(あ 漢   å ★).map(&:ord))
114
+ examples.each do |ch|
115
+ expect(consonants.call(ch)).to match_array([])
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::IO do
5
+ describe '.read_int' do
6
+ let(:value) { rand(-100..100) }
7
+
8
+ it { is_expected.to respond_to(:read_int) }
9
+
10
+ it 'reads an integer' do
11
+ allow($stdin).to receive(:gets).with(no_args).once
12
+ .and_return("#{value}\n")
13
+ expect(subject.read_int).to be(value)
14
+ end
15
+ end
16
+
17
+ describe '.read_chr' do
18
+ it { is_expected.to respond_to(:read_chr) }
19
+
20
+ it 'reads an ASCII code' do
21
+ [0, *32..126].each do |value|
22
+ allow($stdin).to receive(:getc).with(no_args).once
23
+ .and_return(value.chr)
24
+ expect(subject.read_chr).to be(value)
25
+ end
26
+ end
27
+
28
+ it 'reads an control character' do
29
+ [*1..31, *127..159].each do |value|
30
+ allow($stdin).to receive(:getc).with(no_args).once
31
+ .and_return(value.chr)
32
+ expect(subject.read_chr).to be(value)
33
+ end
34
+ end
35
+
36
+ it 'reads an unicode character' do
37
+ [*10.times.map { rand(160..0xD7FF).chr(Encoding::UTF_8) }.uniq,
38
+ # Range 0xD800..0xDFFF is used by UTF-16.
39
+ *10.times.map { rand(0xE000..0x10FFFF).chr(Encoding::UTF_8) }.uniq,
40
+ '가', 'あ', '漢', ' ', 'å', '★'].each do |chr|
41
+ allow($stdin).to receive(:getc).with(no_args).once
42
+ .and_return(chr)
43
+ expect(subject.read_chr).to be(chr.ord)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '.print_int' do
49
+ let(:value) { rand(-100..100) }
50
+
51
+ it { is_expected.to respond_to(:print_int) }
52
+
53
+ it 'prints an integer' do
54
+ allow($stdout).to receive(:print).with(value).once
55
+ expect(subject.print_int(value)).to be_nil
56
+ end
57
+ end
58
+
59
+ describe '.print_chr' do
60
+ it { is_expected.to respond_to(:print_chr) }
61
+
62
+ it 'prints an ASCII character corresponding character code' do
63
+ [0, *32..126].each do |value|
64
+ allow($stdout).to receive(:print).with(value.chr).once
65
+ expect(subject.print_chr(value)).to be_nil
66
+ end
67
+ end
68
+
69
+ it 'prints an control character corresponding character code' do
70
+ [*1..31, *127..159].each do |value|
71
+ allow($stdout).to receive(:print).with(value.chr(Encoding::UTF_8)).once
72
+ expect(subject.print_chr(value)).to be_nil
73
+ end
74
+ end
75
+
76
+ it 'prints an unicode character corresponding character code' do
77
+ [*10.times.map { rand(160..0xD7FF).chr(Encoding::UTF_8) }.uniq,
78
+ # Range 0xD800..0xDFFF is used by UTF-16.
79
+ *10.times.map { rand(0xE000..0x10FFFF).chr(Encoding::UTF_8) }.uniq,
80
+ '가', 'あ', '漢', ' ', 'å', '★'].each do |chr|
81
+ allow($stdout).to receive(:print).with(chr).once
82
+ expect(subject.print_chr(chr.ord)).to be_nil
83
+ end
84
+ end
85
+
86
+ it 'prints an [U+%04X] string when RangeError is raised' do
87
+ [*10.times.map { rand(0xD800..0xDFFF) }.uniq,
88
+ 0x110000].each do |value|
89
+ allow($stdout).to receive(:print).with(format('[U+%04X]', value)).once
90
+ expect(subject.print_chr(value)).to be_nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Option do
5
+ include ExitCodeMatchers
6
+
7
+ subject(:option) { described_class.new }
8
+
9
+ before(:example) { $stdout = StringIO.new }
10
+ after(:example) { $stdout = STDOUT }
11
+
12
+ describe 'option' do
13
+ describe '-h/--help' do
14
+ it 'exits cleanly' do
15
+ expect { option.parse(['-h']) }.to exit_with_code(0)
16
+ expect { option.parse(['--help']) }.to exit_with_code(0)
17
+ end
18
+
19
+ it 'shows help text' do
20
+ begin
21
+ option.parse(['--help'])
22
+ rescue SystemExit # rubocop:disable Lint/HandleExceptions
23
+ end
24
+
25
+ expected_help = <<-END
26
+ Usage: raheui [options] [file]
27
+ -h, --help Print this message.
28
+ -v, --version Print version.
29
+ END
30
+
31
+ expect($stdout.string).to eq(expected_help)
32
+ end
33
+ end
34
+
35
+ describe '-v/--version' do
36
+ it 'exits cleanly' do
37
+ expect { option.parse(['-v']) }.to exit_with_code(0)
38
+ expect { option.parse(['--version']) }.to exit_with_code(0)
39
+ expect($stdout.string).to eq("#{Raheui::Version::STRING}\n" * 2)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Raheui::Port do
5
+ it_behaves_like 'a store'
6
+ end