neuroncheck 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/Rakefile.rb +54 -0
- data/lib/neuroncheck.rb +1 -0
- data/lib/neuroncheck/builtin_keyword.rb +165 -0
- data/lib/neuroncheck/cond_block.rb +42 -0
- data/lib/neuroncheck/declaration.rb +295 -0
- data/lib/neuroncheck/error.rb +13 -0
- data/lib/neuroncheck/kernel.rb +618 -0
- data/lib/neuroncheck/matcher.rb +245 -0
- data/lib/neuroncheck/plugin.rb +149 -0
- data/lib/neuroncheck/syntax.rb +49 -0
- data/lib/neuroncheck/utils.rb +92 -0
- data/lib/neuroncheck/version.rb +3 -0
- data/test/test_advanced_syntactical.rb +125 -0
- data/test/test_inheritance.rb +153 -0
- data/test/test_main.rb +943 -0
- data/test/test_main_syntax.rb +237 -0
- data/test/test_plugin.rb +88 -0
- metadata +105 -0
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'neuroncheck/error'
|
2
|
+
require 'neuroncheck/utils'
|
3
|
+
require 'neuroncheck/plugin'
|
4
|
+
|
5
|
+
module NeuronCheckSystem
|
6
|
+
# 期待する値に対応する、適切なマッチャを取得
|
7
|
+
def self.get_appropriate_matcher(expected, declared_caller_locations)
|
8
|
+
case expected
|
9
|
+
when DeclarationContext # 誤って「self」と記載した場合
|
10
|
+
raise DeclarationError, "`self` cannot be used in declaration - use `:self` instead"
|
11
|
+
when :self # self
|
12
|
+
SelfMatcher.new(declared_caller_locations) # 値がselfであるかどうかチェック
|
13
|
+
|
14
|
+
when String, Symbol, Integer
|
15
|
+
ValueEqualMatcher.new(expected, declared_caller_locations) # 値が等しいかどうかをチェック
|
16
|
+
when true, false, nil
|
17
|
+
ObjectIdenticalMathcer.new(expected, declared_caller_locations) # オブジェクトが同一かどうかをチェック
|
18
|
+
when Class, Module
|
19
|
+
KindOfMatcher.new(expected, declared_caller_locations) # 所属/継承しているかどうかをチェック
|
20
|
+
when Range
|
21
|
+
RangeMatcher.new(expected, declared_caller_locations) # 範囲チェック
|
22
|
+
when Regexp
|
23
|
+
RegexpMatcher.new(expected, declared_caller_locations) # 正規表現チェック
|
24
|
+
# when Encoding
|
25
|
+
# EncodingMatcher.new(expected, declared_caller_locations) # エンコーディングチェック
|
26
|
+
|
27
|
+
when Array
|
28
|
+
OrMatcher.new(expected, declared_caller_locations) # ORチェック
|
29
|
+
|
30
|
+
when Plugin::Keyword # プラグインによって登録されたキーワードの場合
|
31
|
+
KeywordPluginMatcher.new(expected, declared_caller_locations)
|
32
|
+
|
33
|
+
else
|
34
|
+
raise DeclarationError, "#{expected.class.name} cannot be usable for NeuronCheck check parameter\n value: #{expected.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class MatcherBase
|
40
|
+
attr_accessor :declared_caller_locations
|
41
|
+
|
42
|
+
def initialize(expected, declared_caller_locations)
|
43
|
+
@expected = expected
|
44
|
+
@declared_caller_locations = declared_caller_locations
|
45
|
+
end
|
46
|
+
|
47
|
+
def match?(value, self_object)
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_error_message(signature_decl, context_caption, value, phrase_after_but: 'was')
|
52
|
+
locs = Utils.backtrace_locations_to_captions(@declared_caller_locations)
|
53
|
+
|
54
|
+
ret = ""
|
55
|
+
ret.concat(<<MSG)
|
56
|
+
#{context_caption} must be #{expected_caption}, but #{phrase_after_but} #{Utils.truncate(value.inspect, 40)}
|
57
|
+
got: #{value.inspect}
|
58
|
+
MSG
|
59
|
+
|
60
|
+
if signature_decl and signature_decl.assigned_method then
|
61
|
+
ret.concat(<<MSG)
|
62
|
+
signature: #{signature_decl.signature_caption}
|
63
|
+
MSG
|
64
|
+
end
|
65
|
+
|
66
|
+
if locs.size >= 1 then
|
67
|
+
ret.concat(<<MSG)
|
68
|
+
declared at: #{locs.join("\n" + ' ' * 15)}
|
69
|
+
|
70
|
+
MSG
|
71
|
+
end
|
72
|
+
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
|
+
def expected_caption
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
80
|
+
def expected_short_caption
|
81
|
+
@expected.inspect
|
82
|
+
end
|
83
|
+
|
84
|
+
def meta_info_as_json
|
85
|
+
re = {}
|
86
|
+
re['type'] = self.class.name.split('::')[-1]
|
87
|
+
re['expected'] = @expected
|
88
|
+
|
89
|
+
re
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# 値が == で等しいことを判定する
|
94
|
+
class ValueEqualMatcher < MatcherBase
|
95
|
+
def match?(value, self_object)
|
96
|
+
@expected == value
|
97
|
+
end
|
98
|
+
|
99
|
+
def expected_caption
|
100
|
+
@expected.inspect
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# オブジェクトとして同一であることを判定する
|
105
|
+
class ObjectIdenticalMathcer < MatcherBase
|
106
|
+
def match?(value, self_object)
|
107
|
+
@expected.equal?(value)
|
108
|
+
end
|
109
|
+
|
110
|
+
def expected_caption
|
111
|
+
@expected.inspect
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# 値が指定されたClass / Moduleに所属していることを判定する (kind_of?判定)
|
116
|
+
class KindOfMatcher < MatcherBase
|
117
|
+
def match?(value, self_object)
|
118
|
+
value.kind_of?(@expected)
|
119
|
+
end
|
120
|
+
|
121
|
+
def expected_caption
|
122
|
+
@expected.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def meta_info_as_json
|
126
|
+
super.update('expected' => @expected.name)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# 指定した範囲に含まれている値であることを判定する
|
132
|
+
class RangeMatcher < MatcherBase
|
133
|
+
def match?(value, self_object)
|
134
|
+
@expected.include?(value)
|
135
|
+
end
|
136
|
+
|
137
|
+
def expected_caption
|
138
|
+
"included in #{@expected.inspect}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# 指定した正規表現にマッチする文字列であることを判定する
|
143
|
+
class RegexpMatcher < MatcherBase
|
144
|
+
def match?(value, self_object)
|
145
|
+
(value.kind_of?(String) and @expected =~ value)
|
146
|
+
end
|
147
|
+
|
148
|
+
def expected_caption
|
149
|
+
"String that matches with #{@expected.inspect}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# # 指定したエンコーディングを持つ文字列であることを判定する
|
154
|
+
# class EncodingMatcher < MatcherBase
|
155
|
+
# def match?(value, self_object)
|
156
|
+
# (value.kind_of?(String) and Encoding.compatible?(value, @expected))
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# def expected_caption
|
160
|
+
# "String that is compatible #{@expected.name} encoding"
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# def expected_short_caption
|
164
|
+
# "String compatible to #{@expected.name}"
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# end
|
168
|
+
|
169
|
+
# OR条件。渡されたマッチャ複数のうち、どれか1つでも条件を満たせばOK
|
170
|
+
class OrMatcher < MatcherBase
|
171
|
+
def initialize(child_expecteds, declared_caller_locations)
|
172
|
+
@child_matchers = child_expecteds.map{|x| NeuronCheckSystem.get_appropriate_matcher(x, declared_caller_locations)}
|
173
|
+
@declared_caller_locations = declared_caller_locations
|
174
|
+
end
|
175
|
+
|
176
|
+
def match?(value, self_object)
|
177
|
+
# どれか1つにマッチすればOK
|
178
|
+
@child_matchers.any?{|x| x.match?(value, self_object)}
|
179
|
+
end
|
180
|
+
|
181
|
+
def expected_caption
|
182
|
+
captions = @child_matchers.map{|x| x.expected_caption}
|
183
|
+
|
184
|
+
Utils.string_join_using_or_conjunction(captions)
|
185
|
+
end
|
186
|
+
def expected_short_caption
|
187
|
+
'[' + @child_matchers.map{|x| x.expected_short_caption}.join(', ') + ']'
|
188
|
+
end
|
189
|
+
|
190
|
+
def meta_info_as_json
|
191
|
+
super.update('child_matchers' => @child_matchers.map{|x| x.meta_info_as_json})
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# selfであるかどうかを判定 (通常はreturns用)
|
197
|
+
class SelfMatcher < MatcherBase
|
198
|
+
def initialize(declared_caller_locations)
|
199
|
+
@declared_caller_locations = declared_caller_locations
|
200
|
+
end
|
201
|
+
|
202
|
+
def match?(value, self_object)
|
203
|
+
self_object.equal?(value)
|
204
|
+
end
|
205
|
+
|
206
|
+
def expected_caption
|
207
|
+
"self"
|
208
|
+
end
|
209
|
+
def expected_short_caption
|
210
|
+
"self"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# プラグインで追加されたキーワード用
|
215
|
+
class KeywordPluginMatcher < MatcherBase
|
216
|
+
def initialize(keyword, declared_caller_locations)
|
217
|
+
@keyword = keyword
|
218
|
+
@declared_caller_locations = declared_caller_locations
|
219
|
+
end
|
220
|
+
|
221
|
+
def match?(value, self_object)
|
222
|
+
@keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations, self_object)
|
223
|
+
@keyword.match?(value)
|
224
|
+
end
|
225
|
+
|
226
|
+
def expected_caption
|
227
|
+
@keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
|
228
|
+
@keyword.expected_caption
|
229
|
+
end
|
230
|
+
|
231
|
+
def expected_short_caption
|
232
|
+
@keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
|
233
|
+
@keyword.expected_short_caption
|
234
|
+
end
|
235
|
+
|
236
|
+
def meta_info_as_json
|
237
|
+
@keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
|
238
|
+
super.update('keyword' => keyword_name, 'expected_caption' => expected_caption).tap{|x| x.delete('expected')}.update(@keyword.get_params_as_json)
|
239
|
+
end
|
240
|
+
|
241
|
+
def keyword_name
|
242
|
+
(@keyword.class).instance_variable_get(:@keyword_name).to_s
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module NeuronCheckSystem
|
2
|
+
# 追加されたキーワード全てが動的に定義されていくモジュール
|
3
|
+
module Keywords
|
4
|
+
end
|
5
|
+
|
6
|
+
module Plugin
|
7
|
+
|
8
|
+
# NeuronCheckへ登録したキーワード(Symbol)と、それに対応するKeywordクラスの対応を格納したマップ
|
9
|
+
KEYWORD_CLASSES = {}
|
10
|
+
|
11
|
+
# キーワードの追加
|
12
|
+
def self.add_keyword(name, &block)
|
13
|
+
# すでに予約済みのキーワードであればエラー
|
14
|
+
if KEYWORD_CLASSES[name] then
|
15
|
+
raise PluginError, "the `#{name}' keyword has been already reserved"
|
16
|
+
end
|
17
|
+
|
18
|
+
# キーワードを表すクラスを作成
|
19
|
+
keyword_class = Class.new(Keyword, &block)
|
20
|
+
|
21
|
+
# 必要なインスタンスメソッドが全て定義されているかどうかを確認
|
22
|
+
if not keyword_class.method_defined?(:on_call) or
|
23
|
+
not keyword_class.method_defined?(:match?) or
|
24
|
+
not keyword_class.method_defined?(:expected_caption) then
|
25
|
+
raise PluginError, "##{__callee__} requires 3 method definitions - `on_call', `match?' and `expected_caption'"
|
26
|
+
end
|
27
|
+
|
28
|
+
# キーワードを登録
|
29
|
+
keyword_class.instance_variable_set(:@keyword_name, name)
|
30
|
+
KEYWORD_CLASSES[name] = keyword_class
|
31
|
+
|
32
|
+
# キーワードメソッドを定義
|
33
|
+
__define_keyword_method_to_module(name, keyword_class)
|
34
|
+
end
|
35
|
+
|
36
|
+
# キーワードの別名を定義
|
37
|
+
def self.alias_keyword(name, original_keyword_name)
|
38
|
+
# すでに予約済みのキーワードであればエラー
|
39
|
+
if KEYWORD_CLASSES[name] then
|
40
|
+
raise PluginError, "the `#{name}' keyword has been already reserved"
|
41
|
+
end
|
42
|
+
|
43
|
+
# 元キーワードが、自分が追加したキーワードの中になければエラー
|
44
|
+
unless KEYWORD_CLASSES[original_keyword_name] then
|
45
|
+
raise PluginError, "the `#{original_keyword_name}' keyword hasn't been reserved yet"
|
46
|
+
end
|
47
|
+
|
48
|
+
# 継承して別名クラスを作成
|
49
|
+
keyword_class = Class.new(KEYWORD_CLASSES[original_keyword_name])
|
50
|
+
keyword_class.instance_variable_set(:@keyword_name, name)
|
51
|
+
KEYWORD_CLASSES[name] = keyword_class
|
52
|
+
|
53
|
+
# キーワードメソッドを定義
|
54
|
+
__define_keyword_method_to_module(name, keyword_class)
|
55
|
+
end
|
56
|
+
|
57
|
+
# キーワード用のメソッドをKeywordモジュールへ定義する
|
58
|
+
def self.__define_keyword_method_to_module(name, keyword_class)
|
59
|
+
Keywords.module_eval do
|
60
|
+
define_method(name) do |*params|
|
61
|
+
# キーワードを生成
|
62
|
+
kw = keyword_class.new
|
63
|
+
|
64
|
+
# そのキーワードのon_callメソッドを実行
|
65
|
+
kw.on_call(*params)
|
66
|
+
|
67
|
+
# キーワードを返す
|
68
|
+
kw
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# キーワードの削除
|
74
|
+
def self.remove_keyword(name)
|
75
|
+
# 自分が追加したキーワードの中になければエラー
|
76
|
+
unless KEYWORD_CLASSES[name] then
|
77
|
+
raise PluginError, "the `#{name}' keyword hasn't been reserved yet"
|
78
|
+
end
|
79
|
+
|
80
|
+
# 組み込みキーワードを削除使用とした場合はエラー
|
81
|
+
if KEYWORD_CLASSES[name].builtin_keyword? then
|
82
|
+
raise PluginError, "the `#{name}' keyword cannot be removed because it is NeuronCheck builtin keyword"
|
83
|
+
end
|
84
|
+
|
85
|
+
# キーワードを表すクラスを削除
|
86
|
+
KEYWORD_CLASSES.delete(name)
|
87
|
+
|
88
|
+
# キーワードメソッドの定義を削除
|
89
|
+
Keywords.module_eval do
|
90
|
+
remove_method(name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# キーワードクラス
|
95
|
+
class Keyword
|
96
|
+
attr_accessor :api
|
97
|
+
|
98
|
+
def expected_short_caption
|
99
|
+
expected_caption
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_params_as_json
|
103
|
+
{}
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.builtin_keyword?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# キーワードの処理内で使用可能なAPI
|
112
|
+
class KeywordAPI
|
113
|
+
def initialize(declared_caller_locations, method_self_object = nil)
|
114
|
+
@declared_caller_locations = declared_caller_locations
|
115
|
+
@method_self_object = method_self_object
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_appropriate_matcher(expected_value)
|
119
|
+
NeuronCheckSystem.get_appropriate_matcher(expected_value, @declared_caller_locations)
|
120
|
+
end
|
121
|
+
|
122
|
+
def expected_value_match?(value, expected_value)
|
123
|
+
get_appropriate_matcher(expected_value).match?(value, @method_self_object)
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_expected_value_caption(expected_value)
|
127
|
+
get_appropriate_matcher(expected_value).expected_caption
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_expected_value_short_caption(expected_value)
|
132
|
+
get_appropriate_matcher(expected_value).expected_short_caption
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_expected_value_meta_info_as_json(expected_value)
|
137
|
+
get_appropriate_matcher(expected_value).meta_info_as_json
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
module NeuronCheck
|
145
|
+
# プラグイン有効化
|
146
|
+
def self.enable_plugin(plugin_name)
|
147
|
+
require "neuroncheck/plugin/#{plugin_name}"
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'neuroncheck/builtin_keyword'
|
2
|
+
|
3
|
+
# NeuronCheckSyntaxはruby 2.0以前では使用しない (Refinementが実験的機能として定義されているため)
|
4
|
+
unless RUBY_VERSION <= '2.0.9' then
|
5
|
+
NeuronCheckSystem::RUBY_TOPLEVEL = self
|
6
|
+
|
7
|
+
module NeuronCheckSyntax
|
8
|
+
refine Module do
|
9
|
+
# NeuronCheckの宣言用キーワードを、コード内の全箇所で使用可能にする
|
10
|
+
include NeuronCheckSystem::Keywords
|
11
|
+
|
12
|
+
# ndecl宣言 (このメソッドは初回実行時のみ呼び出されることに注意。1度ndeclを実行したら、次以降はNeuronCheckSystem::DeclarationMethodsの方が有効になるため、そちらが呼ばれる)
|
13
|
+
def ndecl(*expecteds, &block)
|
14
|
+
# モジュール/クラス内の場合の処理
|
15
|
+
# extend NeuronCheckが実行されていない未初期化の場合、NeuronCheck用の初期化を自動実行
|
16
|
+
unless @__neuron_check_extended then
|
17
|
+
extend NeuronCheck
|
18
|
+
end
|
19
|
+
|
20
|
+
# メイン処理実行
|
21
|
+
__neuroncheck_ndecl_main(expecteds, block, caller(1, 1))
|
22
|
+
end
|
23
|
+
|
24
|
+
alias ndeclare ndecl
|
25
|
+
alias ncheck ndecl
|
26
|
+
alias ntypesig ndecl
|
27
|
+
alias nsig ndecl
|
28
|
+
|
29
|
+
alias decl ndecl
|
30
|
+
alias declare ndecl
|
31
|
+
alias sig ndecl
|
32
|
+
end
|
33
|
+
|
34
|
+
# トップレベル定義のエイリアス
|
35
|
+
refine NeuronCheckSystem::RUBY_TOPLEVEL.singleton_class do
|
36
|
+
def decl(*expecteds, &block)
|
37
|
+
ndecl(*expecteds, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def declare(*expecteds, &block)
|
41
|
+
ndecl(*expecteds, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def sig(*expecteds, &block)
|
45
|
+
ndecl(*expecteds, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module NeuronCheckSystem
|
2
|
+
module Utils
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# From ActiveSupport (Thanks for Rails Team!) <https://github.com/rails/rails/tree/master/activesupport>
|
6
|
+
#
|
7
|
+
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
|
8
|
+
#
|
9
|
+
# 'Once upon a time in a world far far away'.truncate(27)
|
10
|
+
# # => "Once upon a time in a wo..."
|
11
|
+
#
|
12
|
+
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
|
13
|
+
#
|
14
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
|
15
|
+
# # => "Once upon a time in a..."
|
16
|
+
#
|
17
|
+
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
|
18
|
+
# # => "Once upon a time in a..."
|
19
|
+
#
|
20
|
+
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
|
21
|
+
# for a total length not exceeding <tt>length</tt>:
|
22
|
+
#
|
23
|
+
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
|
24
|
+
# # => "And they f... (continued)"
|
25
|
+
def truncate(str, truncate_at, omission: '...', separator: nil)
|
26
|
+
return str.dup unless str.length > truncate_at
|
27
|
+
|
28
|
+
omission = omission || '...'
|
29
|
+
length_with_room_for_omission = truncate_at - omission.length
|
30
|
+
stop = \
|
31
|
+
if separator
|
32
|
+
rindex(separator, length_with_room_for_omission) || length_with_room_for_omission
|
33
|
+
else
|
34
|
+
length_with_room_for_omission
|
35
|
+
end
|
36
|
+
|
37
|
+
"#{str[0, stop]}#{omission}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# 1つ以上の文字列をorで結んだ英語文字列にする
|
41
|
+
def string_join_using_or_conjunction(strings)
|
42
|
+
ret = ""
|
43
|
+
strings.each_with_index do |str, i|
|
44
|
+
case i
|
45
|
+
when 0 # 最初の要素
|
46
|
+
when strings.size - 1 # 最後の要素
|
47
|
+
ret << " or "
|
48
|
+
else
|
49
|
+
ret << ", "
|
50
|
+
end
|
51
|
+
|
52
|
+
ret << str
|
53
|
+
end
|
54
|
+
|
55
|
+
ret
|
56
|
+
end
|
57
|
+
|
58
|
+
# Thread::Backtrace::Locationのリストを文字列形式に変換。フレーム数が多すぎる場合は途中を省略
|
59
|
+
def backtrace_locations_to_captions(locations)
|
60
|
+
locs = nil
|
61
|
+
if locations.size > 9 then
|
62
|
+
locs = (locations[0..3].map{|x| "from #{x.to_s}"} + [" ... (#{locations.size - 8} frames) ..."] + locations[-4..-1].map{|x| "from #{x.to_s}"})
|
63
|
+
else
|
64
|
+
locs = locations.map{|x| "from #{x.to_s}"}
|
65
|
+
end
|
66
|
+
|
67
|
+
if locs.size >= 1 then
|
68
|
+
locs.first.sub!(/\A\s*from /, '')
|
69
|
+
end
|
70
|
+
|
71
|
+
locs
|
72
|
+
end
|
73
|
+
|
74
|
+
# 指定した整数値を序数文字列にする
|
75
|
+
def ordinalize(v)
|
76
|
+
if [11,12,13].include?(v % 100)
|
77
|
+
"#{v}th"
|
78
|
+
else
|
79
|
+
case (v % 10)
|
80
|
+
when 1
|
81
|
+
"#{v}st"
|
82
|
+
when 2
|
83
|
+
"#{v}nd"
|
84
|
+
when 3
|
85
|
+
"#{v}rd"
|
86
|
+
else
|
87
|
+
"#{v}th"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|