user_agent_parser 1.0.2 → 2.1.3
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/Readme.md +52 -28
- data/bin/user_agent_parser +56 -0
- data/lib/user_agent_parser.rb +4 -15
- data/lib/user_agent_parser/cli.rb +54 -0
- data/lib/user_agent_parser/device.rb +23 -0
- data/lib/user_agent_parser/operating_system.rb +20 -18
- data/lib/user_agent_parser/parser.rb +106 -49
- data/lib/user_agent_parser/user_agent.rb +20 -16
- data/lib/user_agent_parser/version.rb +30 -20
- data/vendor/ua-parser/regexes.yaml +385 -135
- metadata +11 -10
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: a721f503d9e59cd7813d9c18db733ef6ecb24c5e
|
|
4
|
+
data.tar.gz: 7acb12c8b075c1f2cae73b8e3ce29023e5bae740
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d929dfb2af11c55a29a51cb81ae1a3210327275aed9c45c4055374d95d7d1957d6a446ec6cb3c78b9db90911ca7a041d66ca03ecae9e72a0fa423267005d1bc6
|
|
7
|
+
data.tar.gz: 482194e192ffaac970357316d7ee7976b1add1c18d922de35210d82018d4b3dd003723072abb6f37556c24011a72f20b2cb1820c5af06cad7ec71696485eee01
|
data/Readme.md
CHANGED
|
@@ -4,9 +4,7 @@ UserAgentParser is a simple, comprehensive Ruby gem for parsing user agent strin
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
* Ruby >= 1.
|
|
8
|
-
|
|
9
|
-
Note: Ruby 1.8.7 is not supported due to the requirement for the newer psych YAML parser. If you can get it working on 1.8.7 please send a pull request.
|
|
7
|
+
* Ruby >= 1.8.7
|
|
10
8
|
|
|
11
9
|
## Installation
|
|
12
10
|
|
|
@@ -19,48 +17,62 @@ $ gem install user_agent_parser
|
|
|
19
17
|
```ruby
|
|
20
18
|
require 'user_agent_parser'
|
|
21
19
|
=> true
|
|
22
|
-
|
|
20
|
+
user_agent = UserAgentParser.parse 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0;)'
|
|
23
21
|
=> #<UserAgentParser::UserAgent IE 9.0 (Windows Vista)>
|
|
24
|
-
|
|
22
|
+
user_agent.to_s
|
|
25
23
|
=> "IE 9.0"
|
|
26
|
-
|
|
24
|
+
user_agent.name
|
|
27
25
|
=> "IE"
|
|
28
|
-
|
|
26
|
+
user_agent.version.to_s
|
|
29
27
|
=> "9.0"
|
|
30
|
-
|
|
31
|
-
=> 9
|
|
32
|
-
|
|
33
|
-
=> 0
|
|
34
|
-
|
|
28
|
+
user_agent.version.major
|
|
29
|
+
=> "9"
|
|
30
|
+
user_agent.version.minor
|
|
31
|
+
=> "0"
|
|
32
|
+
operating_system = user_agent.os
|
|
35
33
|
=> #<UserAgentParser::OperatingSystem Windows Vista>
|
|
36
|
-
|
|
34
|
+
operating_system.to_s
|
|
37
35
|
=> "Windows Vista"
|
|
36
|
+
|
|
37
|
+
# The parser database will be loaded and parsed on every call to
|
|
38
|
+
# UserAgentParser.parse. To avoid this, instantiate your own Parser instance.
|
|
39
|
+
parser = UserAgentParser::Parser.new
|
|
40
|
+
parser.parse 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0;)'
|
|
41
|
+
=> #<UserAgentParser::UserAgent IE 9.0 (Windows Vista)>
|
|
42
|
+
parser.parse 'Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.5.24 Version/10.53'
|
|
43
|
+
=> #<UserAgentParser::UserAgent Opera 10.53 (Windows XP)>
|
|
38
44
|
```
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
In a larger application, you could store a parser in a global to avoid repeat pattern loading:
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
```ruby
|
|
49
|
+
module MyApplication
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
# Instantiate the parser on load as it's quite expensive
|
|
52
|
+
USER_AGENT_PARSER = UserAgentParser::Parser.new
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
def self.user_agent_parser
|
|
55
|
+
USER_AGENT_PARSER
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
48
59
|
```
|
|
49
60
|
|
|
50
|
-
##
|
|
61
|
+
## The pattern database
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Finished tests in 144.220280s, 89.0027 tests/s, 234.9739 assertions/s.
|
|
63
|
+
The [ua-parser database](https://github.com/tobie/ua-parser/blob/master/regexes.yaml) is included via a [git submodule](http://help.github.com/submodules/). To update the database the submodule needs to be updated and the gem re-released (pull requests for this are very welcome!).
|
|
64
|
+
|
|
65
|
+
You can also specify the path to your own, updated and/or customised `regexes.yaml` file as a second argument to `UserAgentParser.parse`:
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
```ruby
|
|
68
|
+
UserAgentParser.parse(ua_string, patterns_path: '/some/path/to/regexes.yaml')
|
|
59
69
|
```
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
or when instantiating a `UserAgentParser::Parser`:
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
```ruby
|
|
74
|
+
UserAgentParser::Parser.new(patterns_path: '/some/path/to/regexes.yaml').parse(ua_string)
|
|
75
|
+
```
|
|
64
76
|
|
|
65
77
|
## Contributing
|
|
66
78
|
|
|
@@ -71,6 +83,18 @@ There's no support for providing overrides from Javascript user agent detection
|
|
|
71
83
|
|
|
72
84
|
All accepted pull requests will earn you commit and release rights.
|
|
73
85
|
|
|
86
|
+
## Releasing a new version
|
|
87
|
+
|
|
88
|
+
1. Update the version in `user_agent_parser.gemspec`
|
|
89
|
+
2. `git commit user_agent_parser.gemspec` with the following message format:
|
|
90
|
+
|
|
91
|
+
Version x.x.x
|
|
92
|
+
|
|
93
|
+
Changelog:
|
|
94
|
+
* Some new feature
|
|
95
|
+
* Some new bug fix
|
|
96
|
+
3. `rake release`
|
|
97
|
+
|
|
74
98
|
## License
|
|
75
99
|
|
|
76
|
-
MIT
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$: << File.expand_path('../../lib', File.readlink(__FILE__))
|
|
4
|
+
|
|
5
|
+
require 'optparse'
|
|
6
|
+
|
|
7
|
+
require 'user_agent_parser'
|
|
8
|
+
require 'user_agent_parser/cli'
|
|
9
|
+
|
|
10
|
+
options = {}
|
|
11
|
+
|
|
12
|
+
optparse = OptionParser.new do|opts|
|
|
13
|
+
opts.on('--name', 'Print name only') do
|
|
14
|
+
options[:name] = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
opts.on('--version', 'Print version only') do
|
|
18
|
+
options[:version] = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
opts.on('--major', 'Print major version only') do
|
|
22
|
+
options[:major] = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on('--minor', 'Print minor version only') do
|
|
26
|
+
options[:minor] = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on('--os', 'Print operating system only') do
|
|
30
|
+
options[:os] = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on('--format format',
|
|
34
|
+
'Print output in specified format. The available formatters are:',
|
|
35
|
+
' - %n: name',
|
|
36
|
+
' - %v: version',
|
|
37
|
+
' - %M: major version',
|
|
38
|
+
' - %m: minor version',
|
|
39
|
+
' - %o: operating system'
|
|
40
|
+
) do |format|
|
|
41
|
+
options[:format] = format
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
opts.on('-h', '--help', 'Display this screen') do
|
|
45
|
+
puts opts
|
|
46
|
+
exit
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
optparse.parse!
|
|
51
|
+
|
|
52
|
+
parser = UserAgentParser::Parser.new
|
|
53
|
+
|
|
54
|
+
ARGF.each do |line|
|
|
55
|
+
puts UserAgentParser::Cli.new(parser.parse(line), options).run!
|
|
56
|
+
end
|
data/lib/user_agent_parser.rb
CHANGED
|
@@ -2,24 +2,13 @@ require 'user_agent_parser/parser'
|
|
|
2
2
|
require 'user_agent_parser/user_agent'
|
|
3
3
|
require 'user_agent_parser/version'
|
|
4
4
|
require 'user_agent_parser/operating_system'
|
|
5
|
+
require 'user_agent_parser/device'
|
|
5
6
|
|
|
6
7
|
module UserAgentParser
|
|
7
|
-
|
|
8
|
-
# Path to the ua-parser regexes pattern database
|
|
9
|
-
def self.patterns_path
|
|
10
|
-
@patterns_path
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Sets the path to the ua-parser regexes pattern database
|
|
14
|
-
def self.patterns_path=(path)
|
|
15
|
-
@patterns_path = path
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
self.patterns_path = File.join(File.dirname(__FILE__), "../vendor/ua-parser/regexes.yaml")
|
|
8
|
+
DefaultPatternsPath = File.join(File.dirname(__FILE__), "../vendor/ua-parser/regexes.yaml")
|
|
19
9
|
|
|
20
10
|
# Parse the given +user_agent_string+, returning a +UserAgent+
|
|
21
|
-
def self.parse
|
|
22
|
-
Parser.new.parse
|
|
11
|
+
def self.parse(user_agent_string, options={})
|
|
12
|
+
Parser.new(options).parse(user_agent_string)
|
|
23
13
|
end
|
|
24
|
-
|
|
25
14
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module UserAgentParser
|
|
2
|
+
class Cli
|
|
3
|
+
def initialize(user_agent, options = {})
|
|
4
|
+
@user_agent = user_agent
|
|
5
|
+
@options = options
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def run!
|
|
9
|
+
if @options[:name]
|
|
10
|
+
@user_agent.name
|
|
11
|
+
elsif @options[:version]
|
|
12
|
+
with_version do |version|
|
|
13
|
+
version.to_s
|
|
14
|
+
end
|
|
15
|
+
elsif @options[:major]
|
|
16
|
+
major
|
|
17
|
+
elsif @options[:minor]
|
|
18
|
+
minor
|
|
19
|
+
elsif @options[:os]
|
|
20
|
+
@user_agent.os.to_s
|
|
21
|
+
elsif format = @options[:format]
|
|
22
|
+
format.gsub('%n', @user_agent.name).
|
|
23
|
+
gsub('%v', version.to_s).
|
|
24
|
+
gsub('%M', major.to_s).
|
|
25
|
+
gsub('%m', minor.to_s).
|
|
26
|
+
gsub('%o', @user_agent.os.to_s)
|
|
27
|
+
else
|
|
28
|
+
@user_agent.to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def major
|
|
35
|
+
with_version do |version|
|
|
36
|
+
version.major
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def minor
|
|
41
|
+
with_version do |version|
|
|
42
|
+
version.minor
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def version
|
|
47
|
+
@version ||= @user_agent.version
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_version(&block)
|
|
51
|
+
block.call(version) if version
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module UserAgentParser
|
|
2
|
+
class Device
|
|
3
|
+
attr_reader :name
|
|
4
|
+
|
|
5
|
+
def initialize(name = nil)
|
|
6
|
+
@name = name || 'Other'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_s
|
|
10
|
+
name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def inspect
|
|
14
|
+
"#<#{self.class} #{to_s}>"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def eql?(other)
|
|
18
|
+
self.class.eql?(other.class) && name == other.name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias_method :==, :eql?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
module UserAgentParser
|
|
2
|
-
|
|
3
2
|
class OperatingSystem
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
self.version = version
|
|
3
|
+
attr_reader :name, :version
|
|
4
|
+
|
|
5
|
+
def initialize(name = 'Other', version = nil)
|
|
6
|
+
@name = name
|
|
7
|
+
@version = version
|
|
10
8
|
end
|
|
11
|
-
|
|
9
|
+
|
|
12
10
|
def to_s
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
string = name
|
|
12
|
+
unless version.nil?
|
|
13
|
+
string += " #{version}"
|
|
14
|
+
end
|
|
15
|
+
string
|
|
16
16
|
end
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def inspect
|
|
19
19
|
"#<#{self.class} #{to_s}>"
|
|
20
20
|
end
|
|
21
|
-
|
|
22
|
-
def
|
|
23
|
-
|
|
21
|
+
|
|
22
|
+
def eql?(other)
|
|
23
|
+
self.class.eql?(other.class) &&
|
|
24
|
+
name == other.name &&
|
|
25
|
+
version == other.version
|
|
24
26
|
end
|
|
25
|
-
|
|
26
|
-
end
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
alias_method :==, :eql?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
2
|
|
|
3
3
|
module UserAgentParser
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
class Parser
|
|
6
|
+
attr_reader :patterns_path
|
|
6
7
|
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ua
|
|
8
|
+
def initialize(options={})
|
|
9
|
+
@patterns_path = options[:patterns_path] || UserAgentParser::DefaultPatternsPath
|
|
10
|
+
@ua_patterns, @os_patterns, @device_patterns = load_patterns(patterns_path)
|
|
11
11
|
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
12
|
|
|
15
|
-
def
|
|
16
|
-
|
|
13
|
+
def parse(user_agent)
|
|
14
|
+
os = parse_os(user_agent)
|
|
15
|
+
device = parse_device(user_agent)
|
|
16
|
+
parse_ua(user_agent, os, device)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def load_patterns(path)
|
|
22
|
+
yml = YAML.load_file(path)
|
|
23
|
+
|
|
24
|
+
# Parse all the regexs
|
|
25
|
+
yml.each_pair do |type, patterns|
|
|
26
|
+
patterns.each do |pattern|
|
|
27
|
+
pattern["regex"] = Regexp.new(pattern["regex"])
|
|
24
28
|
end
|
|
25
29
|
end
|
|
30
|
+
|
|
31
|
+
[ yml["user_agent_parsers"], yml["os_parsers"], yml["device_parsers"] ]
|
|
26
32
|
end
|
|
27
|
-
|
|
28
|
-
def parse_ua
|
|
29
|
-
pattern, match = first_pattern_match(
|
|
33
|
+
|
|
34
|
+
def parse_ua(user_agent, os = nil, device = nil)
|
|
35
|
+
pattern, match = first_pattern_match(@ua_patterns, user_agent)
|
|
36
|
+
|
|
30
37
|
if match
|
|
31
|
-
user_agent_from_pattern_match(pattern, match)
|
|
38
|
+
user_agent_from_pattern_match(pattern, match, os, device)
|
|
32
39
|
else
|
|
33
|
-
UserAgent.new
|
|
40
|
+
UserAgent.new(nil, nil, os, device)
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
|
-
|
|
37
|
-
def parse_os
|
|
38
|
-
pattern, match = first_pattern_match(
|
|
43
|
+
|
|
44
|
+
def parse_os(user_agent)
|
|
45
|
+
pattern, match = first_pattern_match(@os_patterns, user_agent)
|
|
46
|
+
|
|
39
47
|
if match
|
|
40
48
|
os_from_pattern_match(pattern, match)
|
|
41
49
|
else
|
|
@@ -43,45 +51,94 @@ module UserAgentParser
|
|
|
43
51
|
end
|
|
44
52
|
end
|
|
45
53
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
def parse_device(user_agent)
|
|
55
|
+
pattern, match = first_pattern_match(@device_patterns, user_agent)
|
|
56
|
+
|
|
57
|
+
if match
|
|
58
|
+
device_from_pattern_match(pattern, match)
|
|
59
|
+
else
|
|
60
|
+
Device.new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def first_pattern_match(patterns, value)
|
|
65
|
+
patterns.each do |pattern|
|
|
66
|
+
if match = pattern["regex"].match(value)
|
|
67
|
+
return [pattern, match]
|
|
50
68
|
end
|
|
51
69
|
end
|
|
52
70
|
nil
|
|
53
71
|
end
|
|
54
72
|
|
|
55
|
-
def user_agent_from_pattern_match
|
|
56
|
-
|
|
73
|
+
def user_agent_from_pattern_match(pattern, match, os = nil, device = nil)
|
|
74
|
+
name, v1, v2, v3, v4 = match[1], match[2], match[3], match[4], match[5]
|
|
75
|
+
|
|
57
76
|
if pattern["family_replacement"]
|
|
58
|
-
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
name = pattern["family_replacement"].sub('$1', name || '')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if pattern["v1_replacement"]
|
|
81
|
+
v1 = pattern["v1_replacement"].sub('$1', v1 || '')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if pattern["v2_replacement"]
|
|
85
|
+
v2 = pattern["v2_replacement"].sub('$1', v2 || '')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if pattern["v3_replacement"]
|
|
89
|
+
v3 = pattern["v3_replacement"].sub('$1', v3 || '')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if pattern["v4_replacement"]
|
|
93
|
+
v4 = pattern["v4_replacement"].sub('$1', v4 || '')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
version = version_from_segments(v1, v2, v3, v4)
|
|
97
|
+
|
|
98
|
+
UserAgent.new(name, version, os, device)
|
|
66
99
|
end
|
|
67
|
-
|
|
68
|
-
def os_from_pattern_match
|
|
100
|
+
|
|
101
|
+
def os_from_pattern_match(pattern, match)
|
|
69
102
|
os, v1, v2, v3, v4 = match[1], match[2], match[3], match[4], match[5]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
|
|
104
|
+
if pattern["os_replacement"]
|
|
105
|
+
os = pattern["os_replacement"].sub('$1', os || '')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if pattern["os_v1_replacement"]
|
|
109
|
+
v1 = pattern["os_v1_replacement"].sub('$1', v1 || '')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if pattern["os_v2_replacement"]
|
|
113
|
+
v2 = pattern["os_v2_replacement"].sub('$1', v2 || '')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if pattern["os_v3_replacement"]
|
|
117
|
+
v3 = pattern["os_v3_replacement"].sub('$1', v3 || '')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if pattern["os_v4_replacement"]
|
|
121
|
+
v4 = pattern["os_v4_replacement"].sub('$1', v4 || '')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
version = version_from_segments(v1, v2, v3, v4)
|
|
125
|
+
|
|
126
|
+
OperatingSystem.new(os, version)
|
|
78
127
|
end
|
|
79
|
-
|
|
128
|
+
|
|
129
|
+
def device_from_pattern_match(pattern, match)
|
|
130
|
+
device = match[1]
|
|
131
|
+
|
|
132
|
+
if pattern["device_replacement"]
|
|
133
|
+
device = pattern["device_replacement"].sub('$1', device || '')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Device.new(device)
|
|
137
|
+
end
|
|
138
|
+
|
|
80
139
|
def version_from_segments(*segments)
|
|
81
140
|
version_string = segments.compact.join(".")
|
|
82
141
|
version_string.empty? ? nil : Version.new(version_string)
|
|
83
142
|
end
|
|
84
|
-
|
|
85
143
|
end
|
|
86
|
-
|
|
87
144
|
end
|