PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/measurement/src/measurement.erl

https://github.com/babo/jungerl
Erlang | 348 lines | 178 code | 45 blank | 125 comment | 7 complexity | b1b1f029ec56ada029a15853f1bbe2b3 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause, AGPL-1.0
  1. %%% BEGIN measurement.erl %%%
  2. %%%
  3. %%% measurement - Measurement ADT for Erlang
  4. %%% Copyright (c)2003 Cat's Eye Technologies. All rights reserved.
  5. %%%
  6. %%% Redistribution and use in source and binary forms, with or without
  7. %%% modification, are permitted provided that the following conditions
  8. %%% are met:
  9. %%%
  10. %%% Redistributions of source code must retain the above copyright
  11. %%% notice, this list of conditions and the following disclaimer.
  12. %%%
  13. %%% Redistributions in binary form must reproduce the above copyright
  14. %%% notice, this list of conditions and the following disclaimer in
  15. %%% the documentation and/or other materials provided with the
  16. %%% distribution.
  17. %%%
  18. %%% Neither the name of Cat's Eye Technologies nor the names of its
  19. %%% contributors may be used to endorse or promote products derived
  20. %%% from this software without specific prior written permission.
  21. %%%
  22. %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  23. %%% CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
  24. %%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  25. %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  26. %%% DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
  27. %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
  28. %%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  29. %%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  30. %%% OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  31. %%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  32. %%% OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  33. %%% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  34. %%% POSSIBILITY OF SUCH DAMAGE.
  35. %% @doc Measurement ADT for Erlang.
  36. %%
  37. %% <p>This module implements an abstract data type in Erlang which
  38. %% represents a <i>measurement</i> taken from the observed world.
  39. %% These measurements have, as part of their make-up, both a <i>scalar</i>
  40. %% component which determines their magnitude, and a <i>units of
  41. %% measurement</i> (<i>UoM</i>) component which determines their
  42. %% dimensionality (and may further determine their magnitude within
  43. %% that dimensionality.)</p>
  44. %%
  45. %% <p>Simple arithmetic may be done on measurements, including all the
  46. %% usual operations (addition, subtraction, multiplication, division,
  47. %% and exponentiation,) and during these operations both the scalar
  48. %% component and the UoM component are handled correctly.</p>
  49. %%
  50. %% <p>This module is by no means complete. Many features must be added
  51. %% to it in order to make this ADT reasonably versatile. These include:
  52. %% <ul>
  53. %% <li>converting between different types of compatible units:
  54. %% <ul>
  55. %% <li>explicit conversions, such as between feet and meters</li>
  56. %% <li>implicit conversions, such as when feet are added to meters</li>
  57. %% <li>UoM aliasing, such as between Joules and kg-m^2/sec^2</li>
  58. %% </ul></li>
  59. %% <li>tracking error (tolerance)</li>
  60. %% <li>parsing measurements from strings</li>
  61. %% </ul></p>
  62. %%
  63. %% @end
  64. -module(measurement).
  65. -vsn('JUNGERL').
  66. -author('catseye@catseye.mb.ca').
  67. -copyright('Copyright (c)2003 Cat`s Eye Technologies. All rights reserved.').
  68. -export([new/1, new/2]).
  69. -export([scalar/1, unit/1]).
  70. -export([equal/2, unequal/2, greater_than/2, less_than/2,
  71. greater_than_or_equal/2, less_than_or_equal/2]).
  72. -export([add/2, subtract/2, multiply/2, divide/2, exponent/2]).
  73. -export([format/1, fwrite/2]).
  74. -record(measurement,
  75. {
  76. scalar, % floating point value representing scalar portion
  77. uom % dictionary where dimensions are keys and powers are values
  78. }).
  79. %%% CONSTRUCTORS %%%
  80. %% @spec new(number() | unit()) -> measurement()
  81. %% @doc Creates and returns a new measurement object, either a scalar
  82. %% if a number is given, or a measurement of one unit if a unit is given.
  83. new(S) when atom(S) -> new(1, [{S, 1}]);
  84. new(S) -> new(S, []).
  85. %% @spec new(number(), [{unit(), dimension()}]) -> measurement()
  86. %% unit() = atom()
  87. %% dimension() = integer()
  88. %% measurement() = measurement()
  89. %% @doc Creates and returns a new measurement object with both a scalar
  90. %% and a unit component.
  91. new(S, L) when list(L) ->
  92. #measurement{
  93. scalar = S,
  94. uom = dict:from_list(L)
  95. }.
  96. %%% PRIMITIVES %%%
  97. %% @spec scalar(measurement()) -> number()
  98. %% @doc Extracts the scalar portion of a measurement.
  99. scalar(M) when record(M, measurement) ->
  100. M#measurement.scalar.
  101. %% @spec unit(measurement()) -> measurement()
  102. %% @doc Extracts the unit of a measurement (another measurement whose scalar
  103. %% is one).
  104. unit(M) when record(M, measurement) ->
  105. #measurement{scalar = 1, uom = M#measurement.uom}.
  106. %%% COMPARISON OPERATIONS %%%
  107. %% @spec equal(measurement(), measurement()) -> true | false | {error, Reason}
  108. %% @doc Determines whether two measurements are equal.
  109. equal(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  110. case M1#measurement.uom == M2#measurement.uom of
  111. true ->
  112. M1#measurement.scalar == M2#measurement.scalar;
  113. false ->
  114. {error, incompatible_units}
  115. end;
  116. equal(M1, M2) when record(M1, measurement) -> equal(M1, new(M2));
  117. equal(M1, M2) when record(M2, measurement) -> equal(new(M1), M2);
  118. equal(M1, M2) -> M1 == M2.
  119. %% @spec unequal(measurement(), measurement()) ->
  120. %% true | false | {error, Reason}
  121. %% @doc Determines whether two measurements are not equal.
  122. unequal(M1, M2) ->
  123. case equal(M1, M2) of
  124. true -> false;
  125. false -> true;
  126. Else -> Else
  127. end.
  128. %% @spec greater_than(measurement(), measurement()) ->
  129. %% true | false | {error, Reason}
  130. %% @doc Determines whether the first measurement is greater than the second.
  131. greater_than(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  132. case M1#measurement.uom == M2#measurement.uom of
  133. true ->
  134. M1#measurement.scalar > M2#measurement.scalar;
  135. false ->
  136. {error, incompatible_units}
  137. end;
  138. greater_than(M1, M2) when record(M1, measurement) -> greater_than(M1, new(M2));
  139. greater_than(M1, M2) when record(M2, measurement) -> greater_than(new(M1), M2);
  140. greater_than(M1, M2) -> M1 > M2.
  141. %% @spec greater_than_or_equal(measurement(), measurement()) ->
  142. %% true | false | {error, Reason}
  143. %% @doc Determines whether the first measurement is greater than
  144. %% or equal to the second.
  145. greater_than_or_equal(M1, M2) ->
  146. case greater_than(M1, M2) of
  147. true -> true;
  148. false -> equal(M1, M2);
  149. Else -> Else
  150. end.
  151. %% @spec less_than(measurement(), measurement()) ->
  152. %% true | false | {error, Reason}
  153. %% @doc Determines whether the first measurement is less than the second.
  154. less_than(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  155. case M1#measurement.uom == M2#measurement.uom of
  156. true ->
  157. M1#measurement.scalar < M2#measurement.scalar;
  158. false ->
  159. {error, incompatible_units}
  160. end;
  161. less_than(M1, M2) when record(M1, measurement) -> less_than(M1, new(M2));
  162. less_than(M1, M2) when record(M2, measurement) -> less_than(new(M1), M2);
  163. less_than(M1, M2) -> M1 < M2.
  164. %% @spec less_than_or_equal(measurement(), measurement()) ->
  165. %% true | false | {error, Reason}
  166. %% @doc Determines whether the first measurement is less than
  167. %% or equal to the second.
  168. less_than_or_equal(M1, M2) ->
  169. case less_than(M1, M2) of
  170. true -> true;
  171. false -> equal(M1, M2);
  172. Else -> Else
  173. end.
  174. %%% ORDER 0 ARITHMETIC OPERATIONS %%%
  175. %% @spec add(measurement(), measurement()) ->
  176. %% measurement() | {error, Reason}
  177. %% @doc Yields the sum of two measurements.
  178. add(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  179. case M1#measurement.uom == M2#measurement.uom of
  180. true ->
  181. #measurement{
  182. scalar = M1#measurement.scalar + M2#measurement.scalar,
  183. uom = M1#measurement.uom
  184. };
  185. false ->
  186. {error, incompatible_units}
  187. end;
  188. add(M1, M2) when record(M1, measurement) -> add(M1, new(M2));
  189. add(M1, M2) when record(M2, measurement) -> add(new(M1), M2);
  190. add(M1, M2) when atom(M1) -> add(new(M1), M2);
  191. add(M1, M2) when atom(M2) -> add(M1, new(M2));
  192. add(M1, M2) -> M1 + M2.
  193. %% @spec subtract(measurement(), measurement()) ->
  194. %% measurement() | {error, Reason}
  195. %% @doc Yields the difference between two measurements.
  196. subtract(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  197. case M1#measurement.uom == M2#measurement.uom of
  198. true ->
  199. #measurement{
  200. scalar = M1#measurement.scalar - M2#measurement.scalar,
  201. uom = M1#measurement.uom
  202. };
  203. false ->
  204. {error, incompatible_units}
  205. end;
  206. subtract(M1, M2) when record(M1, measurement) -> subtract(M1, new(M2));
  207. subtract(M1, M2) when record(M2, measurement) -> subtract(new(M1), M2);
  208. subtract(M1, M2) when atom(M1) -> subtract(new(M1), M2);
  209. subtract(M1, M2) when atom(M2) -> subtract(M1, new(M2));
  210. subtract(M1, M2) -> M1 - M2.
  211. %%% ORDER 1 ARITHMETIC OPERATIONS %%%
  212. %% @spec multiply(measurement(), measurement()) ->
  213. %% measurement() | {error, Reason}
  214. %% @doc Yields the product of two measurements.
  215. multiply(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  216. #measurement{
  217. scalar = M1#measurement.scalar * M2#measurement.scalar,
  218. uom = dict:filter(fun(_,V) -> V =/= 0 end,
  219. dict:merge(fun(_,A,B) -> A + B end,
  220. M1#measurement.uom, M2#measurement.uom))
  221. };
  222. multiply(M1, M2) when record(M1, measurement) -> multiply(M1, new(M2));
  223. multiply(M1, M2) when record(M2, measurement) -> multiply(new(M1), M2);
  224. multiply(M1, M2) when atom(M1) -> multiply(new(M1), M2);
  225. multiply(M1, M2) when atom(M2) -> multiply(M1, new(M2));
  226. multiply(M1, M2) -> M1 * M2.
  227. %% @spec divide(measurement(), measurement()) ->
  228. %% measurement() | {error, Reason}
  229. %% @doc Yields the ratio between two measurements.
  230. divide(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  231. #measurement{
  232. scalar = M1#measurement.scalar / M2#measurement.scalar,
  233. uom = dict:filter(fun(_,V) -> V =/= 0 end,
  234. dict:merge(fun(_,A,B) -> A + B end, M1#measurement.uom,
  235. dict:map(fun(K,V) -> -V end, M2#measurement.uom)))
  236. };
  237. divide(M1, M2) when record(M1, measurement) -> divide(M1, new(M2));
  238. divide(M1, M2) when record(M2, measurement) -> divide(new(M1), M2);
  239. divide(M1, M2) when atom(M1) -> divide(new(M1), M2);
  240. divide(M1, M2) when atom(M2) -> divide(M1, new(M2));
  241. divide(M1, M2) -> M1 / M2.
  242. %%% ORDER 2 ARITHMETIC OPERATIONS %%%
  243. %% @spec exponent(measurement(), measurement()) ->
  244. %% measurement() | {error, Reason}
  245. %% @doc Raises a measurement to the power of another measurement.
  246. %% This only produces a sensible result if the second measurement
  247. %% is a scalar and an integer.
  248. exponent(M1, M2) when record(M1, measurement), record(M2, measurement) ->
  249. % count key/value pairs in M2's unit dictionary
  250. case dict:fold(fun(_,_,A) -> A + 1 end, 0, M2#measurement.uom) of
  251. 0 ->
  252. case M2#measurement.scalar of
  253. I when is_integer(I) ->
  254. #measurement{
  255. scalar = math:pow(M1#measurement.scalar, M2#measurement.scalar),
  256. uom = dict:filter(fun(_,V) -> V =/= 0 end,
  257. dict:map(fun(_,V) -> V * M2#measurement.scalar end,
  258. M1#measurement.uom))
  259. };
  260. _ ->
  261. {error, exponent_must_be_integer}
  262. end;
  263. _ ->
  264. {error, exponent_must_be_scalar}
  265. end;
  266. exponent(M1, M2) when record(M1, measurement) -> exponent(M1, new(M2));
  267. exponent(M1, M2) when record(M2, measurement) -> exponent(new(M1), M2);
  268. exponent(M1, M2) when atom(M1) -> exponent(new(M1), M2);
  269. exponent(M1, M2) when atom(M2) -> exponent(M1, new(M2));
  270. exponent(M1, M2) -> math:pow(M1, M2).
  271. %% @spec format(measurement()) -> string()
  272. %% @doc Render a measurement in human-readable form.
  273. format(M) when record(M, measurement) ->
  274. NumerUom = dict:filter(fun(K, V) when V > 0 -> true; (K, V) -> false end,
  275. M#measurement.uom),
  276. DenomUom = dict:map(fun(K,V) -> -V end,
  277. dict:filter(fun(K, V) when V < 0 -> true; (K, V) -> false end,
  278. M#measurement.uom)),
  279. NumerCount = length(dict:fetch_keys(NumerUom)),
  280. DenomCount = length(dict:fetch_keys(DenomUom)),
  281. Fun = fun(K, V, A) ->
  282. case V of
  283. 1 -> A ++ io_lib:fwrite("~w ", [K]);
  284. _ -> A ++ io_lib:fwrite("~w^~w ", [K, V])
  285. end
  286. end,
  287. UnitString = case {NumerCount, DenomCount} of
  288. {0, 0} -> "";
  289. {NC, 0} -> dict:fold(Fun, [], NumerUom);
  290. {0, DC} -> "/" ++ dict:fold(Fun, [], DenomUom);
  291. {NC,DC} -> dict:fold(Fun, [], NumerUom) ++ "/" ++
  292. dict:fold(Fun, [], DenomUom)
  293. end,
  294. io_lib:fwrite("~w ~s", [scalar(M), UnitString]);
  295. format(M) -> io_lib:fwrite("~w", [M]).
  296. %% @spec fwrite(string(), [term()]) -> ok
  297. %% @doc Wrapper for <code>io:fwrite</code> which is sensitive to measurements.
  298. fwrite(Format, List) ->
  299. io:fwrite(Format, lists:map(fun(M) when record(M, measurement) ->
  300. format(M);
  301. (M) -> M
  302. end, List)).
  303. %%% END of measurement.erl %%%