basic_temperature 0.2.1 → 0.2.2

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.
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