basic_temperature 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +33 -0
  3. data/.coveralls.yml +1 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +12 -0
  6. data/.travis.yml +2 -1
  7. data/README.md +223 -9
  8. data/basic_temperature.gemspec +6 -1
  9. data/bin/console +4 -4
  10. data/docs/apple-touch-icon.png +0 -0
  11. data/docs/classes/BasicTemperature.html +1227 -0
  12. data/docs/classes/BasicTemperature/InitializationArgumentsError.html +94 -0
  13. data/docs/classes/BasicTemperature/InvalidDegreesError.html +94 -0
  14. data/docs/classes/BasicTemperature/InvalidNumericOrTemperatureError.html +94 -0
  15. data/docs/classes/BasicTemperature/InvalidScaleError.html +94 -0
  16. data/docs/classes/Object.html +274 -0
  17. data/docs/classes/Temperature.html +1227 -0
  18. data/docs/created.rid +4 -0
  19. data/docs/css/github.css +123 -0
  20. data/docs/css/main.css +374 -0
  21. data/docs/css/panel.css +361 -0
  22. data/docs/css/reset.css +48 -0
  23. data/docs/favicon.ico +0 -0
  24. data/docs/files/lib/basic_temperature/temperature_rb.html +73 -0
  25. data/docs/files/lib/basic_temperature/version_rb.html +84 -0
  26. data/docs/files/lib/basic_temperature_rb.html +112 -0
  27. data/docs/i/arrow-down.svg +8 -0
  28. data/docs/i/arrow-right.svg +8 -0
  29. data/docs/i/search.svg +12 -0
  30. data/docs/i/tree_bg.svg +8 -0
  31. data/docs/index.html +11 -0
  32. data/docs/js/highlight.pack.js +1 -0
  33. data/docs/js/jquery-1.3.2.min.js +19 -0
  34. data/docs/js/main.js +25 -0
  35. data/docs/js/navigation.js +105 -0
  36. data/docs/js/navigation.js.gz +0 -0
  37. data/docs/js/search_index.js +1 -0
  38. data/docs/js/search_index.js.gz +0 -0
  39. data/docs/js/searchdoc.js +465 -0
  40. data/docs/js/searcher.js +229 -0
  41. data/docs/js/searcher.js.gz +0 -0
  42. data/docs/panel/index.html +47 -0
  43. data/docs/panel/links.html +12 -0
  44. data/docs/panel/tree.js +1 -0
  45. data/lib/basic_temperature.rb +552 -75
  46. data/lib/basic_temperature/temperature.rb +5 -0
  47. data/lib/basic_temperature/version.rb +1 -1
  48. data/logo.png +0 -0
  49. metadata +112 -4
  50. data/Gemfile.lock +0 -54
@@ -0,0 +1,229 @@
1
+ Searcher = function(data) {
2
+ this.data = data;
3
+ this.handlers = [];
4
+ }
5
+
6
+ Searcher.prototype = new function() {
7
+ // search is performed in chunks of 1000 for non-blocking user input
8
+ var CHUNK_SIZE = 1000;
9
+ // do not try to find more than 100 results
10
+ var MAX_RESULTS = 100;
11
+ var huid = 1;
12
+ var suid = 1;
13
+ var runs = 0;
14
+
15
+ this.find = function(query) {
16
+ var queries = splitQuery(query);
17
+ var regexps = buildRegexps(queries);
18
+ var highlighters = buildHilighters(queries);
19
+ var state = { from: 0, pass: 0, limit: MAX_RESULTS, n: suid++};
20
+ var _this = this;
21
+
22
+ this.currentSuid = state.n;
23
+
24
+ if (!query) return;
25
+
26
+ var run = function() {
27
+ // stop current search thread if new search started
28
+ if (state.n != _this.currentSuid) return;
29
+
30
+ var results =
31
+ performSearch(_this.data, regexps, queries, highlighters, state);
32
+ var hasMore = (state.limit > 0 && state.pass < 4);
33
+
34
+ triggerResults.call(_this, results, !hasMore);
35
+ if (hasMore) {
36
+ setTimeout(run, 2);
37
+ }
38
+ runs++;
39
+ };
40
+ runs = 0;
41
+
42
+ // start search thread
43
+ run();
44
+ }
45
+
46
+ /* ----- Events ------ */
47
+ this.ready = function(fn) {
48
+ fn.huid = huid;
49
+ this.handlers.push(fn);
50
+ }
51
+
52
+ /* ----- Utilities ------ */
53
+ function splitQuery(query) {
54
+ return query.split(/(\s+|::?|\(\)?)/).filter(function(string) {
55
+ return string.match(/\S/);
56
+ });
57
+ }
58
+
59
+ function buildRegexps(queries) {
60
+ return queries.map(function(query) {
61
+ return new RegExp(query.replace(/(.)/g, '([$1])([^$1]*?)'), 'i');
62
+ });
63
+ }
64
+
65
+ function buildHilighters(queries) {
66
+ return queries.map(function(query) {
67
+ return query.split('').map(function(l, i) {
68
+ return '\u0001$' + (i*2+1) + '\u0002$' + (i*2+2);
69
+ }).join('');
70
+ });
71
+ }
72
+
73
+ // function longMatchRegexp(index, longIndex, regexps) {
74
+ // for (var i = regexps.length - 1; i >= 0; i--){
75
+ // if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
76
+ // };
77
+ // return true;
78
+ // }
79
+
80
+
81
+ /* ----- Mathchers ------ */
82
+
83
+ /*
84
+ * This record matches if the index starts with queries[0] and the record
85
+ * matches all of the regexps
86
+ */
87
+ function matchPassBeginning(index, longIndex, queries, regexps) {
88
+ if (index.indexOf(queries[0]) != 0) return false;
89
+ for (var i=1, l = regexps.length; i < l; i++) {
90
+ if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
91
+ return false;
92
+ };
93
+ return true;
94
+ }
95
+
96
+ /*
97
+ * This record matches if the longIndex starts with queries[0] and the
98
+ * longIndex matches all of the regexps
99
+ */
100
+ function matchPassLongIndex(index, longIndex, queries, regexps) {
101
+ if (longIndex.indexOf(queries[0]) != 0) return false;
102
+ for (var i=1, l = regexps.length; i < l; i++) {
103
+ if (!longIndex.match(regexps[i]))
104
+ return false;
105
+ };
106
+ return true;
107
+ }
108
+
109
+ /*
110
+ * This record matches if the index contains queries[0] and the record
111
+ * matches all of the regexps
112
+ */
113
+ function matchPassContains(index, longIndex, queries, regexps) {
114
+ if (index.indexOf(queries[0]) == -1) return false;
115
+ for (var i=1, l = regexps.length; i < l; i++) {
116
+ if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
117
+ return false;
118
+ };
119
+ return true;
120
+ }
121
+
122
+ /*
123
+ * This record matches if regexps[0] matches the index and the record
124
+ * matches all of the regexps
125
+ */
126
+ function matchPassRegexp(index, longIndex, queries, regexps) {
127
+ if (!index.match(regexps[0])) return false;
128
+ for (var i=1, l = regexps.length; i < l; i++) {
129
+ if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
130
+ return false;
131
+ };
132
+ return true;
133
+ }
134
+
135
+
136
+ /* ----- Highlighters ------ */
137
+ function highlightRegexp(info, queries, regexps, highlighters) {
138
+ var result = createResult(info);
139
+ for (var i=0, l = regexps.length; i < l; i++) {
140
+ result.title = result.title.replace(regexps[i], highlighters[i]);
141
+ result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
142
+ };
143
+ return result;
144
+ }
145
+
146
+ function hltSubstring(string, pos, length) {
147
+ return string.substring(0, pos) + '\u0001' + string.substring(pos, pos + length) + '\u0002' + string.substring(pos + length);
148
+ }
149
+
150
+ function highlightQuery(info, queries, regexps, highlighters) {
151
+ var result = createResult(info);
152
+ var pos = 0;
153
+ var lcTitle = result.title.toLowerCase();
154
+
155
+ pos = lcTitle.indexOf(queries[0]);
156
+ if (pos != -1) {
157
+ result.title = hltSubstring(result.title, pos, queries[0].length);
158
+ }
159
+
160
+ result.namespace = result.namespace.replace(regexps[0], highlighters[0]);
161
+ for (var i=1, l = regexps.length; i < l; i++) {
162
+ result.title = result.title.replace(regexps[i], highlighters[i]);
163
+ result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
164
+ };
165
+ return result;
166
+ }
167
+
168
+ function createResult(info) {
169
+ var result = {};
170
+ result.title = info[0];
171
+ result.namespace = info[1];
172
+ result.path = info[2];
173
+ result.params = info[3];
174
+ result.snippet = info[4];
175
+ result.badge = info[6];
176
+ return result;
177
+ }
178
+
179
+ /* ----- Searching ------ */
180
+ function performSearch(data, regexps, queries, highlighters, state) {
181
+ var searchIndex = data.searchIndex;
182
+ var longSearchIndex = data.longSearchIndex;
183
+ var info = data.info;
184
+ var result = [];
185
+ var i = state.from;
186
+ var l = searchIndex.length;
187
+ var togo = CHUNK_SIZE;
188
+ var matchFunc, hltFunc;
189
+
190
+ while (state.pass < 4 && state.limit > 0 && togo > 0) {
191
+ if (state.pass == 0) {
192
+ matchFunc = matchPassBeginning;
193
+ hltFunc = highlightQuery;
194
+ } else if (state.pass == 1) {
195
+ matchFunc = matchPassLongIndex;
196
+ hltFunc = highlightQuery;
197
+ } else if (state.pass == 2) {
198
+ matchFunc = matchPassContains;
199
+ hltFunc = highlightQuery;
200
+ } else if (state.pass == 3) {
201
+ matchFunc = matchPassRegexp;
202
+ hltFunc = highlightRegexp;
203
+ }
204
+
205
+ for (; togo > 0 && i < l && state.limit > 0; i++, togo--) {
206
+ if (info[i].n == state.n) continue;
207
+ if (matchFunc(searchIndex[i], longSearchIndex[i], queries, regexps)) {
208
+ info[i].n = state.n;
209
+ result.push(hltFunc(info[i], queries, regexps, highlighters));
210
+ state.limit--;
211
+ }
212
+ };
213
+ if (searchIndex.length <= i) {
214
+ state.pass++;
215
+ i = state.from = 0;
216
+ } else {
217
+ state.from = i;
218
+ }
219
+ }
220
+ return result;
221
+ }
222
+
223
+ function triggerResults(results, isLast) {
224
+ this.handlers.forEach(function(fn) {
225
+ fn.call(this, results, isLast)
226
+ });
227
+ }
228
+ }
229
+
Binary file
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>search index</title>
5
+ <link rel="stylesheet" href="../css/reset.css" type="text/css" media="screen" charset="utf-8" />
6
+ <link rel="stylesheet" href="../css/panel.css" type="text/css" media="screen" charset="utf-8" />
7
+ <script src="../js/search_index.js" type="text/javascript" charset="utf-8"></script>
8
+ <script src="../js/searcher.js" type="text/javascript" charset="utf-8"></script>
9
+ <script src="tree.js" type="text/javascript" charset="utf-8"></script>
10
+ <script src="../js/jquery-1.3.2.min.js" type="text/javascript" charset="utf-8"></script>
11
+ <script src="../js/searchdoc.js" type="text/javascript" charset="utf-8"></script>
12
+ <script type="text/javascript" charset="utf-8">
13
+ $(function() {
14
+ $('#links').hide();
15
+ var panel = new Searchdoc.Panel($('#panel'), search_data, tree, top.frames[1]);
16
+ $('#search').focus();
17
+
18
+ var s = window.parent.location.search.match(/\?q=([^&]+)/);
19
+ if (s) {
20
+ s = decodeURIComponent(s[1]).replace(/\+/g, ' ');
21
+ if (s.length > 0) {
22
+ $('#search').val(s);
23
+ panel.search(s, true);
24
+ }
25
+ }
26
+ })
27
+ </script>
28
+ </head>
29
+
30
+ <body>
31
+ <div class="panel panel_tree" id="panel">
32
+ <div class="header">
33
+ <input type="text" placeholder="Search for a class, method, ..." autosave="searchdoc" results="10" id="search" autocomplete="off" />
34
+ </div>
35
+ <div class="tree">
36
+ <ul>
37
+ </ul>
38
+ </div>
39
+ <div class="result">
40
+ <ul>
41
+ </ul>
42
+ </div>
43
+ </div>
44
+ <a href="links.html" id="links">index</a>
45
+ </body>
46
+
47
+ </html>
@@ -0,0 +1,12 @@
1
+ <html>
2
+ <head>File index</head>
3
+ <body>
4
+
5
+ <a href="../files/lib/basic_temperature_rb.html">lib/basic_temperature.rb</a>
6
+
7
+ <a href="../files/lib/basic_temperature/temperature_rb.html">lib/basic_temperature/temperature.rb</a>
8
+
9
+ <a href="../files/lib/basic_temperature/version_rb.html">lib/basic_temperature/version.rb</a>
10
+
11
+ </body>
12
+ </html>
@@ -0,0 +1 @@
1
+ var tree = [["","","files",[["","","lib",[["","","basic_temperature",[["temperature.rb","files/lib/basic_temperature/temperature_rb.html","",[]],["version.rb","files/lib/basic_temperature/version_rb.html","",[]]]],["basic_temperature.rb","files/lib/basic_temperature_rb.html","",[]]]]]],["Temperature","classes/BasicTemperature.html"," < Object",[]],["Object","classes/Object.html"," < BasicObject",[]],["Temperature","classes/Temperature.html"," < Object",[]]]
@@ -2,28 +2,229 @@
2
2
 
3
3
  require 'basic_temperature/version'
4
4
 
5
- # Value Object for basic temperature operations like conversions from Celcius to Fahrenhait or Kelvin etc.
6
5
  # rubocop:disable Metrics/ClassLength
6
+
7
+ ##
8
+ # Temperature is a simple {Value Object}[https://martinfowler.com/bliki/ValueObject.html] for basic
9
+ # temperature operations like conversions from <tt>Celsius</tt> to <tt>Fahrenhait</tt> or <tt>Kelvin</tt>
10
+ # etc.
11
+ #
12
+ # Supported scales: <tt>Celsius</tt>, <tt>Fahrenheit</tt>, <tt>Kelvin</tt> and <tt>Rankine</tt>.
13
+ #
14
+ # == Creating Temperatures
15
+ #
16
+ # A new temperature can be created in multiple ways:
17
+ #
18
+ # - Using keyword arguments:
19
+ #
20
+ # Temperature.new(degrees: 0, scale: :celsius)
21
+ #
22
+ # - Using positional arguments:
23
+ #
24
+ # Temperature.new(0, :celsius)
25
+ #
26
+ # - Even more concise way using <tt>Temperature.[]</tt> (an alias of <tt>Temperature.new</tt>):
27
+ #
28
+ # Temperature[0, :celsius]
29
+ #
30
+ #
31
+ # == Creating Temperatures from already existing temperature objects
32
+ #
33
+ # Sometimes it is useful to create a new temperature from already existing one.
34
+ #
35
+ # For such cases, there are {set_degrees}[rdoc-ref:BasicTemperature#set_degrees and
36
+ # {set_scale}[rdoc-ref:BasicTemperature#set_scale].
37
+ #
38
+ # Since temperatures are {Value Objects}[https://martinfowler.com/bliki/ValueObject.html], both methods
39
+ # returns new instances.
40
+ #
41
+ # Examples:
42
+ #
43
+ # temperature = Temperature[0, :celsius]
44
+ # # => 0 °C
45
+ #
46
+ # new_temperature = temperature.set_degrees(15)
47
+ # # => 15 °C
48
+ #
49
+ # temperature = Temperature[0, :celsius]
50
+ # # => 0 °C
51
+ #
52
+ # new_temperature = temperature.set_scale(:kelvin)
53
+ # # => 0 K
54
+ #
55
+ # == Conversions
56
+ #
57
+ # Temperatures can be converted to diffirent scales.
58
+ #
59
+ # Currently, the following scales are supported: <tt>Celsius</tt>, <tt>Fahrenheit</tt>, <tt>Kelvin</tt> and
60
+ # <tt>Rankine</tt>.
61
+ #
62
+ # Temperature[20, :celsius].to_celsius
63
+ # # => 20 °C
64
+ #
65
+ # Temperature[20, :celsius].to_fahrenheit
66
+ # # => 68 °F
67
+ #
68
+ # Temperature[20, :celsius].to_kelvin
69
+ # # => 293.15 K
70
+ #
71
+ # Temperature[20, :celsius].to_rankine
72
+ # # => 527.67 °R
73
+ #
74
+ # If it is necessary to convert scale dynamically, {to_scale}[rdoc-ref:BasicTemperature#to_scale] method is
75
+ # available.
76
+ #
77
+ # Temperature[20, :celsius].to_scale(scale)
78
+ #
79
+ # All conversion formulas are taken from
80
+ # {RapidTables}[https://www.rapidtables.com/convert/temperature/index.html].
81
+ #
82
+ # Conversion precision: 2 accurate digits after the decimal dot.
83
+ #
84
+ # == Comparison
85
+ #
86
+ # Temperature implements idiomatic {<=> spaceship operator}[https://ruby-doc.org/core/Comparable.html] and
87
+ # mixes in {Comparable}[https://ruby-doc.org/core/Comparable.html] module.
88
+ #
89
+ # As a result, all methods from Comparable are available, e.g:
90
+ #
91
+ # Temperature[20, :celsius] < Temperature[25, :celsius]
92
+ # # => true
93
+ #
94
+ # Temperature[20, :celsius] <= Temperature[25, :celsius]
95
+ # # => true
96
+ #
97
+ # Temperature[20, :celsius] == Temperature[25, :celsius]
98
+ # # => false
99
+ #
100
+ # Temperature[20, :celsius] > Temperature[25, :celsius]
101
+ # # => false
102
+ #
103
+ # Temperature[20, :celsius] >= Temperature[25, :celsius]
104
+ # # => false
105
+ #
106
+ # Temperature[20, :celsius].between?(Temperature[15, :celsius], Temperature[25, :celsius])
107
+ # # => true
108
+ #
109
+ # # Starting from Ruby 2.4.6
110
+ # Temperature[20, :celsius].clamp(Temperature[20, :celsius], Temperature[25, :celsius])
111
+ # # => 20 °C
112
+ #
113
+ # Please note, if <tt>other</tt> temperature has a different scale, temperature is automatically converted
114
+ # to that scale before comparison.
115
+ #
116
+ # Temperature[20, :celsius] == Temperature[293.15, :kelvin]
117
+ # # => true
118
+ #
119
+ # IMPORTANT !!!
120
+ #
121
+ # <tt>degrees</tt> are rounded to the nearest value with a precision of 2 decimal digits before comparison.
122
+ #
123
+ # This means the following temperatures are considered as equal:
124
+ #
125
+ # Temperature[20.020, :celsius] == Temperature[20.024, :celsius]
126
+ # # => true
127
+ #
128
+ # Temperature[20.025, :celsius] == Temperature[20.029, :celsius]
129
+ # # => true
130
+ #
131
+ # while these ones are treated as NOT equal:
132
+ #
133
+ # Temperature[20.024, :celsius] == Temperature[20.029, :celsius]
134
+ # # => false
135
+ #
136
+ # == Math
137
+ #
138
+ # ==== Addition/Subtraction.
139
+ #
140
+ # Temperature[20, :celsius] + Temperature[10, :celsius]
141
+ # # => 30 °C
142
+ #
143
+ # Temperature[20, :celsius] - Temperature[10, :celsius]
144
+ # # => 10 °C
145
+ #
146
+ # If second temperature has a different scale, first temperature is automatically converted to that scale
147
+ # before <tt>degrees</tt> addition/subtraction.
148
+ #
149
+ # Temperature[283.15, :kelvin] + Temperature[10, :celsius]
150
+ # # => 10 °C
151
+ #
152
+ # Returned temperature will have the same scale as the second temperature.
153
+ #
154
+ # It is possible to add/subtract numerics.
155
+ #
156
+ # Temperature[20, :celsius] + 10
157
+ # # => 30 °C
158
+ #
159
+ # Temperature[20, :celsius] - 10
160
+ # # => 10 °C
161
+ #
162
+ # In such cases, returned temperature will have the same scale as the first temperature.
163
+ #
164
+ # Also {Ruby coersion mechanism}[https://ruby-doc.org/core/Numeric.html#method-i-coerce] is supported.
165
+ #
166
+ # 10 + Temperature[20, :celsius]
167
+ # # => 30 °C
168
+ #
169
+ # 10 - Temperature[20, :celsius]
170
+ # # => -10 °C
171
+ #
172
+ # ==== Negation
173
+ #
174
+ # -Temperature[20, :celsius]
175
+ # # => -20 °C
176
+ #
7
177
  class BasicTemperature
178
+ include Comparable
179
+
180
+ # Raised when <tt>Temperature.new</tt> is called with mixed positional and keyword arguments or without
181
+ # arguments at all.
8
182
  class InitializationArgumentsError < StandardError; end
183
+
184
+ # Raised when <tt>degrees</tt> is not a Numeric.
9
185
  class InvalidDegreesError < StandardError; end
186
+
187
+ # Raised when <tt>scale</tt> can not be casted to any possible scale value.
188
+ # See {SCALES}[rdoc-ref:BasicTemperature::SCALES].
10
189
  class InvalidScaleError < StandardError; end
11
- class InvalidOtherError < StandardError; end
12
- class CoersionError < StandardError; end
13
190
 
14
- SCALES =
15
- [
16
- CELCIUS = 'celcius',
17
- FAHRENHEIT = 'fahrenheit',
18
- KELVIN = 'kelvin'
19
- ]
20
- .freeze
191
+ # Raised when <tt>other</tt> is not a Numeric or Temperature in math operations.
192
+ class InvalidNumericOrTemperatureError < StandardError; end
193
+
194
+ CELSIUS = 'celsius'
195
+ FAHRENHEIT = 'fahrenheit'
196
+ KELVIN = 'kelvin'
197
+ RANKINE = 'rankine'
21
198
 
22
- attr_reader :degrees, :scale
199
+ # A list of all currently supported scale values.
200
+ SCALES = [CELSIUS, FAHRENHEIT, KELVIN, RANKINE].freeze
201
+
202
+ # Degrees of the temperature.
203
+ attr_reader :degrees
204
+
205
+ # Scale of the temperature. Look at {SCALES}[rdoc-ref:BasicTemperature::SCALES] for possible values.
206
+ attr_reader :scale
207
+
208
+ ##
209
+ # Creates a new instance of Temperature. Alias for <tt>new</tt>.
210
+ #
211
+ # :call-seq:
212
+ # [](degrees:, scale:)
213
+ # [](degrees, scale)
214
+ #
215
+ def self.[](*args, **kwargs)
216
+ new(*args, **kwargs)
217
+ end
23
218
 
219
+ ##
220
+ # Creates a new instance of Temperature. Is aliased as <tt>[]</tt>.
221
+ #
222
+ # :call-seq:
223
+ # new(degrees:, scale:)
224
+ # new(degrees, scale)
225
+ #
24
226
  def initialize(*positional_arguments, **keyword_arguments)
25
- raise_initialization_arguments_error if positional_arguments.any? && keyword_arguments.any?
26
- raise_initialization_arguments_error if positional_arguments.none? && keyword_arguments.none?
227
+ assert_either_positional_arguments_or_keyword_arguments!(positional_arguments, keyword_arguments)
27
228
 
28
229
  if keyword_arguments.any?
29
230
  initialize_via_keywords_arguments(keyword_arguments)
@@ -33,124 +234,352 @@ class BasicTemperature
33
234
  end
34
235
 
35
236
  # rubocop:disable Naming/AccessorMethodName
237
+
238
+ # Returns a new Temperature with updated <tt>degrees</tt>.
239
+ #
240
+ # temperature = Temperature[0, :celsius]
241
+ # # => 0 °C
242
+ #
243
+ # new_temperature = temperature.set_degrees(15)
244
+ # # => 15 °C
245
+ #
36
246
  def set_degrees(degrees)
37
247
  BasicTemperature.new(degrees, scale)
38
248
  end
39
249
  # rubocop:enable Naming/AccessorMethodName
40
250
 
41
251
  # rubocop:disable Naming/AccessorMethodName
252
+
253
+ # Returns a new Temperature with updated <tt>scale</tt>.
254
+ #
255
+ # temperature = Temperature[0, :celsius]
256
+ # # => 0 °C
257
+ #
258
+ # new_temperature = temperature.set_scale(:kelvin)
259
+ # # => 0 K
260
+ #
42
261
  def set_scale(scale)
43
- self.to_scale(scale)
262
+ BasicTemperature.new(degrees, scale)
44
263
  end
45
264
  # rubocop:enable Naming/AccessorMethodName
46
265
 
266
+ ##
267
+ # Converts temperature to specific <tt>scale</tt>.
268
+ # If temperature is already in desired <tt>scale</tt>, returns current temperature object.
269
+ #
270
+ # Raises {InvalidScaleError}[rdoc-ref:BasicTemperature::InvalidScaleError]
271
+ # when <tt>scale</tt> can not be casted to any possible scale value
272
+ # (see {SCALES}[rdoc-ref:BasicTemperature::SCALES]).
273
+ #
274
+ # Temperature[60, :fahrenheit].to_scale(:celsius)
275
+ # # => 15.56 °C
276
+ #
47
277
  def to_scale(scale)
48
- case cast_scale(scale)
49
- when CELCIUS
278
+ casted_scale = cast_scale(scale)
279
+
280
+ assert_valid_scale!(casted_scale)
281
+
282
+ case casted_scale
283
+ when CELSIUS
50
284
  to_celsius
51
285
  when FAHRENHEIT
52
286
  to_fahrenheit
53
287
  when KELVIN
54
288
  to_kelvin
55
- else
56
- raise_invalid_scale_error
289
+ when RANKINE
290
+ to_rankine
57
291
  end
58
292
  end
59
293
 
294
+ ##
295
+ # Converts temperature to Celsius scale. If temperature is already in Celsius, returns current
296
+ # temperature object.
297
+ #
298
+ # Memoizes subsequent calls.
299
+ #
300
+ # Conversion formulas are taken from {RapidTables}[https://www.rapidtables.com/]:
301
+ # 1. {Celsius to Fahrenheit}[https://www.rapidtables.com/convert/temperature/celsius-to-fahrenheit.html].
302
+ # 2. {Celsius to Kelvin}[https://www.rapidtables.com/convert/temperature/celsius-to-kelvin.html].
303
+ # 3. {Celsius to Rankine}[https://www.rapidtables.com/convert/temperature/celsius-to-rankine.html].
304
+ #
305
+ # Temperature[0, :fahrenheit].to_celsius
306
+ # # => -17.78 °C
307
+ #
60
308
  def to_celsius
61
- return @to_celsius unless @to_celsius.nil?
62
-
63
- return @to_celsius = self if self.scale == CELCIUS
64
-
65
- degrees =
66
- case self.scale
67
- when FAHRENHEIT
68
- (self.degrees - 32) * (5 / 9r)
69
- when KELVIN
70
- self.degrees - 273.15
71
- end
72
-
73
- @to_celsius = BasicTemperature.new(degrees, CELCIUS)
309
+ memoized(:to_celsius) || memoize(:to_celsius, -> {
310
+ return self if self.scale == CELSIUS
311
+
312
+ degrees =
313
+ case self.scale
314
+ when FAHRENHEIT
315
+ (self.degrees - 32) * (5 / 9r)
316
+ when KELVIN
317
+ self.degrees - 273.15
318
+ when RANKINE
319
+ (self.degrees - 491.67) * (5 / 9r)
320
+ end
321
+
322
+ BasicTemperature.new(degrees, CELSIUS)
323
+ })
74
324
  end
75
325
 
326
+ ##
327
+ # Converts temperature to Fahrenheit scale. If temperature is already in Fahrenheit, returns current
328
+ # temperature object.
329
+ #
330
+ # Memoizes subsequent calls.
331
+ #
332
+ # Conversion formulas are taken from {RapidTables}[https://www.rapidtables.com/]:
333
+ # 1. {Fahrenheit to Celsius}[https://www.rapidtables.com/convert/temperature/fahrenheit-to-celsius.html].
334
+ # 2. {Fahrenheit to Kelvin}[https://www.rapidtables.com/convert/temperature/fahrenheit-to-kelvin.html].
335
+ # 3. {Fahrenheit to Rankine}[https://www.rapidtables.com/convert/temperature/fahrenheit-to-rankine.html].
336
+ #
337
+ # Temperature[0, :celsius].to_fahrenheit
338
+ # # => 32 °F
339
+ #
76
340
  def to_fahrenheit
77
- return @to_fahrenheit unless @to_fahrenheit.nil?
78
-
79
- return @to_fahrenheit = self if self.scale == FAHRENHEIT
80
-
81
- degrees =
82
- case self.scale
83
- when CELCIUS
84
- self.degrees * (9 / 5r) + 32
85
- when KELVIN
86
- self.degrees * (9 / 5r) - 459.67
87
- end
88
-
89
- @to_fahrenheit = BasicTemperature.new(degrees, FAHRENHEIT)
341
+ memoized(:to_fahrenheit) || memoize(:to_fahrenheit, -> {
342
+ return self if self.scale == FAHRENHEIT
343
+
344
+ degrees =
345
+ case self.scale
346
+ when CELSIUS
347
+ self.degrees * (9 / 5r) + 32
348
+ when KELVIN
349
+ self.degrees * (9 / 5r) - 459.67
350
+ when RANKINE
351
+ self.degrees - 459.67
352
+ end
353
+
354
+ BasicTemperature.new(degrees, FAHRENHEIT)
355
+ })
90
356
  end
91
357
 
358
+ ##
359
+ # Converts temperature to Kelvin scale. If temperature is already in Kelvin, returns current
360
+ # temperature object.
361
+ #
362
+ # Memoizes subsequent calls.
363
+ #
364
+ # Conversion formulas are taken from {RapidTables}[https://www.rapidtables.com/]:
365
+ # 1. {Kelvin to Celsius}[https://www.rapidtables.com/convert/temperature/kelvin-to-celsius.html].
366
+ # 2. {Kelvin to Fahrenheit}[https://www.rapidtables.com/convert/temperature/kelvin-to-fahrenheit.html].
367
+ # 3. {Kelvin to Rankine}[https://www.rapidtables.com/convert/temperature/kelvin-to-rankine.html].
368
+ #
369
+ # Temperature[0, :kelvin].to_rankine
370
+ # # => 0 °R
371
+ #
92
372
  def to_kelvin
93
- return @to_kelvin unless @to_kelvin.nil?
94
-
95
- return @to_kelvin = self if self.scale == KELVIN
96
-
97
- degrees =
98
- case self.scale
99
- when CELCIUS
100
- self.degrees + 273.15
101
- when FAHRENHEIT
102
- (self.degrees + 459.67) * (5 / 9r)
103
- end
373
+ memoized(:to_kelvin) || memoize(:to_kelvin, -> {
374
+ return self if self.scale == KELVIN
375
+
376
+ degrees =
377
+ case self.scale
378
+ when CELSIUS
379
+ self.degrees + 273.15
380
+ when FAHRENHEIT
381
+ (self.degrees + 459.67) * (5 / 9r)
382
+ when RANKINE
383
+ self.degrees * (5 / 9r)
384
+ end
385
+
386
+ BasicTemperature.new(degrees, KELVIN)
387
+ })
388
+ end
104
389
 
105
- @to_kelvin = BasicTemperature.new(degrees, KELVIN)
390
+ ##
391
+ # Converts temperature to Rankine scale. If temperature is already in Rankine, returns current
392
+ # temperature object.
393
+ #
394
+ # Memoizes subsequent calls.
395
+ #
396
+ # Conversion formulas are taken from {RapidTables}[https://www.rapidtables.com/]:
397
+ # 1. {Rankine to Celsius}[https://www.rapidtables.com/convert/temperature/rankine-to-celsius.html].
398
+ # 2. {Rankine to Fahrenheit}[https://www.rapidtables.com/convert/temperature/rankine-to-fahrenheit.html].
399
+ # 3. {Rankine to Kelvin}[https://www.rapidtables.com/convert/temperature/rankine-to-kelvin.html].
400
+ #
401
+ # Temperature[0, :rankine].to_kelvin
402
+ # # => 0 K
403
+ #
404
+ def to_rankine
405
+ memoized(:to_rankine) || memoize(:to_rankine, -> {
406
+ return self if self.scale == RANKINE
407
+
408
+ degrees =
409
+ case self.scale
410
+ when CELSIUS
411
+ (self.degrees + 273.15) * (9 / 5r)
412
+ when FAHRENHEIT
413
+ self.degrees + 459.67
414
+ when KELVIN
415
+ self.degrees * (9 / 5r)
416
+ end
417
+
418
+ BasicTemperature.new(degrees, RANKINE)
419
+ })
106
420
  end
107
421
 
108
- def ==(other)
109
- return false unless other.instance_of?(BasicTemperature)
422
+ ##
423
+ # Compares temperture with <tt>other</tt> temperature.
424
+ #
425
+ # Returns <tt>0</tt> if they are considered as equal.
426
+ #
427
+ # Two temperatures are considered as equal when they have the same amount of <tt>degrees</tt>.
428
+ #
429
+ # Returns <tt>-1</tt> if temperature is lower than <tt>other</tt> temperature.
430
+ #
431
+ # Returns <tt>1</tt> if temperature is higher than <tt>other</tt> temperature.
432
+ #
433
+ # If <tt>other</tt> temperature has a different scale, temperature is automatically converted to that scale
434
+ # before <tt>degrees</tt> comparison.
435
+ #
436
+ # Temperature[20, :celsius] <=> Temperature[20, :celsius]
437
+ # # => 0
438
+ #
439
+ # Temperature[20, :celsius] <=> Temperature[293.15, :kelvin]
440
+ # # => 0
441
+ #
442
+ # IMPORTANT!!!
443
+ #
444
+ # This method rounds <tt>degrees</tt> to the nearest value with a precision of 2 decimal digits.
445
+ #
446
+ # This means the following:
447
+ #
448
+ # Temperature[20.020, :celsius] <=> Temperature[20.024, :celsius]
449
+ # # => 0
450
+ #
451
+ # Temperature[20.025, :celsius] <=> Temperature[20.029, :celsius]
452
+ # # => 0
453
+ #
454
+ # Temperature[20.024, :celsius] <=> Temperature[20.029, :celsius]
455
+ # # => -1
456
+ #
457
+ def <=>(other)
458
+ return unless assert_temperature(other)
110
459
 
111
- self.degrees == other.degrees && self.scale == other.scale
460
+ compare_degrees(self.to_scale(other.scale).degrees, other.degrees)
112
461
  end
113
462
 
463
+ ##
464
+ # Performs addition. Returns a new Temperature.
465
+ #
466
+ # Temperature[20, :celsius] + Temperature[10, :celsius]
467
+ # # => 30 °C
468
+ #
469
+ # If the second temperature has a different scale, the first temperature is automatically converted to that
470
+ # scale before <tt>degrees</tt> addition.
471
+ #
472
+ # Temperature[283.15, :kelvin] + Temperature[20, :celsius]
473
+ # # => 30 °C
474
+ #
475
+ # Returned temperature will have the same scale as the second temperature.
476
+ #
477
+ # It is possible to add numerics.
478
+ #
479
+ # Temperature[20, :celsius] + 10
480
+ # # => 30 °C
481
+ #
482
+ # In such cases, returned temperature will have the same scale as the first temperature.
483
+ #
484
+ # Also {Ruby coersion mechanism}[https://ruby-doc.org/core/Numeric.html#method-i-coerce] is supported.
485
+ #
486
+ # 10 + Temperature[20, :celsius]
487
+ # # => 30 °C
488
+ #
489
+ # :call-seq:
490
+ # +(temperature)
491
+ # +(numeric)
492
+ #
114
493
  def +(other)
494
+ assert_numeric_or_temperature!(other)
495
+
115
496
  degrees, scale =
116
497
  case other
117
498
  when Numeric
118
499
  [self.degrees + other, self.scale]
119
500
  when BasicTemperature
120
501
  [self.to_scale(other.scale).degrees + other.degrees, other.scale]
121
- else
122
- raise_invalid_other_error(other)
123
502
  end
124
503
 
125
504
  BasicTemperature.new(degrees, scale)
126
505
  end
127
506
 
507
+ ##
508
+ # Performs subtraction. Returns a new Temperature.
509
+ #
510
+ # Temperature[20, :celsius] - Temperature[10, :celsius]
511
+ # # => 10 °C
512
+ #
513
+ # If the second temperature has a different scale, the first temperature is automatically converted to that
514
+ # scale before <tt>degrees</tt> subtraction.
515
+ #
516
+ # Temperature[283.15, :kelvin] + Temperature[10, :celsius]
517
+ # # => 10 °C
518
+ #
519
+ # Returned temperature will have the same scale as the second temperature.
520
+ #
521
+ # It is possible to subtract numerics.
522
+ #
523
+ # Temperature[20, :celsius] - 10
524
+ # # => 10 °C
525
+ #
526
+ # In such cases, returned temperature will have the same scale as the first temperature.
527
+ #
528
+ # Also {Ruby coersion mechanism}[https://ruby-doc.org/core/Numeric.html#method-i-coerce] is supported.
529
+ #
530
+ # 10 - Temperature[20, :celsius]
531
+ # # => -10 °C
532
+ #
533
+ # :call-seq:
534
+ # -(temperature)
535
+ # -(numeric)
536
+ #
128
537
  def -(other)
129
538
  self + -other
130
539
  end
131
540
 
541
+ ##
542
+ # Returns a new Temperature with negated <tt>degrees</tt>.
543
+ #
544
+ # -Temperature[20, :celsius]
545
+ # # => -20 °C
546
+ #
132
547
  def -@
133
548
  BasicTemperature.new(-self.degrees, self.scale)
134
549
  end
135
550
 
136
- def coerce(numeric)
551
+ # Is used by {+}[rdoc-ref:BasicTemperature#+] and {-}[rdoc-ref:BasicTemperature#-]
552
+ # for {Ruby coersion mechanism}[https://ruby-doc.org/core/Numeric.html#method-i-coerce].
553
+ def coerce(numeric) #:nodoc:
137
554
  assert_numeric!(numeric)
138
555
 
139
556
  [BasicTemperature.new(numeric, self.scale), self]
140
557
  end
141
558
 
142
- def inspect
143
- "#{degrees.to_i} #{scale.capitalize}"
144
- end
559
+ # Returns a string containing a human-readable representation of temperature.
560
+ def inspect #:nodoc:
561
+ rounded_degrees = round_degrees(degrees)
145
562
 
146
- def <=>(other)
147
- return unless other.instance_of?(BasicTemperature)
563
+ printable_degrees = degrees_without_decimal?(rounded_degrees) ? rounded_degrees.to_i : rounded_degrees
148
564
 
149
- self.to_scale(other.scale).degrees <=> other.degrees
565
+ scale_symbol =
566
+ case self.scale
567
+ when CELSIUS
568
+ '°C'
569
+ when FAHRENHEIT
570
+ '°F'
571
+ when KELVIN
572
+ 'K'
573
+ when RANKINE
574
+ '°R'
575
+ end
576
+
577
+ "#{printable_degrees} #{scale_symbol}"
150
578
  end
151
579
 
152
580
  private
153
581
 
582
+ # Initialization
154
583
  def initialize_via_positional_arguments(positional_arguments)
155
584
  degrees, scale = positional_arguments
156
585
 
@@ -164,19 +593,31 @@ class BasicTemperature
164
593
  end
165
594
 
166
595
  def initialize_arguments(degrees, scale)
596
+ casted_degrees = cast_degrees(degrees)
167
597
  casted_scale = cast_scale(scale)
168
598
 
169
- assert_valid_degrees!(degrees)
599
+ assert_valid_degrees!(casted_degrees)
170
600
  assert_valid_scale!(casted_scale)
171
601
 
172
- @degrees = degrees
602
+ @degrees = casted_degrees
173
603
  @scale = casted_scale
174
604
  end
175
605
 
606
+ # Casting
607
+ def cast_degrees(degrees)
608
+ Float(degrees) rescue nil
609
+ end
610
+
176
611
  def cast_scale(scale)
177
612
  scale.to_s
178
613
  end
179
614
 
615
+ # Assertions
616
+ def assert_either_positional_arguments_or_keyword_arguments!(positional_arguments, keyword_arguments)
617
+ raise_initialization_arguments_error if positional_arguments.any? && keyword_arguments.any?
618
+ raise_initialization_arguments_error if positional_arguments.none? && keyword_arguments.none?
619
+ end
620
+
180
621
  def assert_valid_degrees!(degrees)
181
622
  raise_invalid_degrees_error unless degrees.is_a?(Numeric)
182
623
  end
@@ -188,13 +629,18 @@ class BasicTemperature
188
629
  def assert_numeric_or_temperature!(numeric_or_temperature)
189
630
  return if numeric_or_temperature.is_a?(Numeric) || numeric_or_temperature.instance_of?(BasicTemperature)
190
631
 
191
- raise_coersion_error(numeric_or_temperature)
632
+ raise_invalid_numeric_or_temperature_error(numeric_or_temperature)
192
633
  end
193
634
 
194
635
  def assert_numeric!(numeric)
195
636
  raise_invalid_numeric unless numeric.is_a?(Numeric)
196
637
  end
197
638
 
639
+ def assert_temperature(temperature)
640
+ temperature.instance_of?(BasicTemperature)
641
+ end
642
+
643
+ # Raising errors
198
644
  def raise_initialization_arguments_error
199
645
  message =
200
646
  'Positional and keyword arguments are mixed or ' \
@@ -215,12 +661,43 @@ class BasicTemperature
215
661
  raise InvalidScaleError, message
216
662
  end
217
663
 
218
- def raise_invalid_other_error(other)
219
- raise InvalidOtherError, "`#{other}` is neither Numeric nor Temperature."
664
+ def raise_invalid_numeric_or_temperature_error(numeric_or_temperature)
665
+ raise InvalidNumericOrTemperatureError, "`#{numeric_or_temperature}` is neither Numeric nor Temperature."
666
+ end
667
+
668
+ # Rounding
669
+ def round_degrees(degrees)
670
+ degrees.round(2)
671
+ end
672
+
673
+ def compare_degrees(first_degrees, second_degrees)
674
+ round_degrees(first_degrees) <=> round_degrees(second_degrees)
675
+ end
676
+
677
+ def degrees_with_decimal?(degrees)
678
+ degrees % 1 != 0
679
+ end
680
+
681
+ def degrees_without_decimal?(degrees)
682
+ !degrees_with_decimal?(degrees)
683
+ end
684
+
685
+ # Memoization
686
+ def memoized(key)
687
+ name = convert_to_variable_name(key)
688
+
689
+ instance_variable_get(name) if instance_variable_defined?(name)
690
+ end
691
+
692
+ def memoize(key, proc)
693
+ name = convert_to_variable_name(key)
694
+ value = proc.call
695
+
696
+ instance_variable_set(name, value)
220
697
  end
221
698
 
222
- def raise_coersion_error(object)
223
- raise CoersionError, "#{object} is neither Numeric nor Temperature."
699
+ def convert_to_variable_name(key)
700
+ "@#{key}"
224
701
  end
225
702
  end
226
703
  # rubocop:enable Metrics/ClassLength