engineering-notation 0.12.0__tar.gz

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.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: engineering-notation
3
+ Version: 0.12.0
4
+ Summary: Easy engineering notation
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ [![Unit Tests](https://github.com/slightlynybbled/engineering_notation/actions/workflows/unittest.yml/badge.svg)](https://github.com/slightlynybbled/engineering_notation/actions/workflows/unittest.yml)
9
+
10
+ # Purpose
11
+
12
+ To easily work with human-readable engineering notation. I wrote this as a quick tool for my own use.
13
+ I found that I was writing the same functionality into multiple packages and would like a quick pip-installable
14
+ package to take care of this manipulation for me. The package should be easily extended for other use cases.
15
+ The package is unit-less, so only operates on numeric values. Unit detection may be added in future versions.
16
+
17
+ More information may be found at [for(embed)](http://forembed.com/engineering-notation-in-python.html).
18
+
19
+ # Installation
20
+
21
+ Install using pip: `pip install engineering_notation`.
22
+
23
+ # Usage
24
+
25
+ There are multiple ways of initializing a number to a particular value, but a string is the preferred method:
26
+
27
+ ```
28
+ >>> from engineering_notation import EngNumber
29
+ >>> EngNumber('10k')
30
+ 10k
31
+ >>> EngNumber('10000')
32
+ 10k
33
+ >>> EngNumber(10000)
34
+ 10k
35
+ >>> EngNumber(10000.0)
36
+ 10k
37
+ >>> EngNumber(1e4)
38
+ 10k
39
+ ```
40
+
41
+ Where decimals are involved, we use a default precision of 2 digits:
42
+
43
+ ```
44
+ >>> EngNumber('4.99k')
45
+ 4.99k
46
+ >>> EngNumber('4.9k')
47
+ 4.90k
48
+ ```
49
+
50
+ This behavior can truncate your results in some cases, and cause your number to round. To specify more or less
51
+ digits, simply specify the precision in the declaration:
52
+
53
+ ```
54
+ >>> EngNumber('4.999k')
55
+ 5k
56
+ >>> EngNumber('4.999k', precision=3)
57
+ 4.999k
58
+ ```
59
+
60
+ Most operations that you would perform on numeric values are valid, although all operations are not implemented:
61
+
62
+ ```
63
+ >>> EngNumber('2.2k') * 2
64
+ 4.40k
65
+ >>> 2 * EngNumber('2.2k')
66
+ 4.40k
67
+ >>> EngNumber(1.2) > EngNumber('3.3k')
68
+ False
69
+ >>> EngNumber(1.2) <= EngNumber('3.3k')
70
+ True
71
+ >>> EngNumber('3.3k') == EngNumber(3300)
72
+ True
73
+ ```
74
+
75
+ All of the above operations are also possible on the `EngUnit()` class as well. The only difference is
76
+ that units must match for addition/subtraction/comparison operations. Although multiplication and division
77
+ operations will work numerically, they may not always be strictly correct. This is because EngUnit is not
78
+ intended to replace a computer algebra system!
79
+
80
+ ```
81
+ >>> EngUnit('2s') / EngUnit('4rotations')
82
+ 0.5s/rotations
83
+ ```
84
+
85
+ Additionally, since there are 'reserved' letters for sizing the number, you must be careful with your units!
86
+
87
+ ```
88
+ >>> EngUnit('2mm')
89
+ 2mm # <<< this value equivalent to "0.002m"
90
+ >>> EngUnit('2meter')
91
+ 2meter # <<< this value is equivalent to "0.002eter", the "m" was used to scale the unit!
92
+ >>> EngUnit('2', unit='meter') # <<< this will work better
93
+ ```
94
+
95
+ # Contributions
96
+
97
+ Contributions are welcome. Feel free to make feature requests in the issues.
98
+
99
+ ## Test Installation
100
+
101
+ If you are developing, you probably want to perform a local editable installation:
102
+
103
+ ```bash
104
+ uv run pip install -e .
105
+ ```
106
+
107
+ ## Testing
108
+
109
+ ```bash
110
+ uv run python -m pytest
111
+ ```
@@ -0,0 +1,4 @@
1
+ from engineering_notation.eng_notation import EngNumber, EngUnit
2
+ from engineering_notation.version import __version__
3
+
4
+ __all__ = ['EngNumber', 'EngUnit', '__version__']
@@ -0,0 +1,555 @@
1
+ from decimal import Decimal
2
+ from string import digits
3
+ import sys
4
+
5
+ from typing import Optional
6
+
7
+ try:
8
+ import numpy
9
+ except ImportError:
10
+ pass
11
+
12
+ _suffix_lookup = {
13
+ 'y': 'e-24',
14
+ 'z': 'e-21',
15
+ 'a': 'e-18',
16
+ 'f': 'e-15',
17
+ 'p': 'e-12',
18
+ 'n': 'e-9',
19
+ 'u': 'e-6',
20
+ 'm': 'e-3',
21
+ '': 'e0',
22
+ 'k': 'e3',
23
+ 'M': 'e6',
24
+ 'G': 'e9',
25
+ 'T': 'e12',
26
+ 'P': 'e15',
27
+ 'E': 'e18',
28
+ 'Z': 'e21',
29
+ }
30
+
31
+ _exponent_lookup_scaled = {
32
+ '-54': 'y',
33
+ '-51': 'z',
34
+ '-48': 'a',
35
+ '-45': 'f',
36
+ '-42': 'p',
37
+ '-39': 'n',
38
+ '-36': 'u',
39
+ '-33': 'm',
40
+ '-30': '',
41
+ '-27': 'k',
42
+ '-24': 'M',
43
+ '-21': 'G',
44
+ '-18': 'T',
45
+ '-15': 'P',
46
+ '-12': 'E',
47
+ '-9': 'Z',
48
+ }
49
+
50
+
51
+ class EngUnit:
52
+ """
53
+ Represents an engineering number, complete with units
54
+ """
55
+ def __init__(self, value,
56
+ precision=2, significant=0, unit: Optional[str] = None, separator=""):
57
+ """
58
+ Initialize engineering with units
59
+ :param value: the desired value in the form of a string, int, or float
60
+ :param precision: the number of decimal places
61
+ :param significant: the number of significant digits
62
+ if given, significant takes precendence over precision
63
+ """
64
+ suffix_keys = [key for key in _suffix_lookup.keys() if key != '']
65
+ self.unit = unit
66
+
67
+ if isinstance(value, str):
68
+ # parse the string into unit and engineering number
69
+ new_value = ''
70
+ v_index = 0
71
+ for c in value:
72
+ if (c in digits) or (c in ['.', '-']) or (c in suffix_keys):
73
+ new_value += c
74
+ v_index += 1
75
+ else:
76
+ break
77
+
78
+ if self.unit is None and len(value) >= v_index:
79
+ self.unit = value[v_index:]
80
+
81
+ self.eng_num = EngNumber(new_value, precision,
82
+ significant, separator)
83
+
84
+ else:
85
+ self.eng_num = EngNumber(value, precision, significant, separator)
86
+
87
+ def __repr__(self):
88
+ """
89
+ Returns the object representation
90
+ :return: a string representing the engineering number
91
+ """
92
+ unit = self.unit if self.unit else ''
93
+ return str(self.eng_num) + unit
94
+
95
+ def __str__(self):
96
+ """
97
+ Returns the string representation
98
+ :return: a string representing the engineering number
99
+ """
100
+ return self.__repr__()
101
+
102
+ def __int__(self):
103
+ """
104
+ Implements the 'int()' method
105
+ :return:
106
+ """
107
+ return int(self.eng_num)
108
+
109
+ def __float__(self):
110
+ """
111
+ Implements the 'float()' method
112
+ :return:
113
+ """
114
+ return float(self.eng_num)
115
+
116
+ def __add__(self, other):
117
+ """
118
+ Add two engineering numbers, with units
119
+ :param other: EngNum, str, float, or int
120
+ :return: result
121
+ """
122
+ if not isinstance(other, EngNumber):
123
+ other = EngUnit(str(other))
124
+
125
+ if self.unit != other.unit:
126
+ raise AttributeError('units do not match')
127
+
128
+ return EngUnit(str(self.eng_num + other.eng_num) + self.unit)
129
+
130
+ def __radd__(self, other):
131
+ """
132
+ Add two engineering numbers, with units
133
+ :param other: EngNum, str, float, or int
134
+ :return: result
135
+ """
136
+ return self.__add__(other)
137
+
138
+ def __sub__(self, other):
139
+ """
140
+ Subtract two engineering numbers, with units
141
+ :param other: EngNum, str, float, or int
142
+ :return: result
143
+ """
144
+ if not isinstance(other, EngNumber):
145
+ other = EngUnit(str(other))
146
+
147
+ if self.unit != other.unit:
148
+ raise AttributeError('units do not match')
149
+
150
+ return EngUnit(str(self.eng_num - other.eng_num) + self.unit)
151
+
152
+ def __rsub__(self, other):
153
+ """
154
+ Subtract two engineering numbers, with units
155
+ :param other: EngNum, str, float, or int
156
+ :return: result
157
+ """
158
+ if not isinstance(other, EngNumber):
159
+ other = EngUnit(str(other))
160
+
161
+ if self.unit != other.unit:
162
+ raise AttributeError('units do not match')
163
+
164
+ return EngUnit(str(other.eng_num - self.eng_num) + self.unit)
165
+
166
+ def __mul__(self, other):
167
+ """
168
+ Multiply two engineering numbers, with units
169
+ :param other: EngNum, str, float, or int
170
+ :return: result
171
+ """
172
+ if not isinstance(other, EngNumber):
173
+ other = EngUnit(str(other))
174
+
175
+ return EngUnit(str(self.eng_num * other.eng_num)
176
+ + self.unit + other.unit)
177
+
178
+ def __rmul__(self, other):
179
+ """
180
+ Multiply two engineering numbers, with units
181
+ :param other: EngNum, str, float, or int
182
+ :return: result
183
+ """
184
+ return self.__mul__(other)
185
+
186
+ def __truediv__(self, other):
187
+ """
188
+ Divide two engineering numbers, with units
189
+ :param other: EngNum, str, float, or int
190
+ :return: result
191
+ """
192
+ if not isinstance(other, EngNumber):
193
+ other = EngUnit(str(other))
194
+
195
+ new_unit = ''
196
+ if self.unit:
197
+ new_unit += self.unit
198
+ if other.unit:
199
+ new_unit += '/' + other.unit
200
+
201
+ return EngUnit(str(self.eng_num / other.eng_num) + new_unit)
202
+
203
+ def __rtruediv__(self, other):
204
+ """
205
+ Divide two engineering numbers, with units
206
+ :param other: EngNum, str, float, or int
207
+ :return: result
208
+ """
209
+ if not isinstance(other, EngNumber):
210
+ other = EngUnit(str(other))
211
+
212
+ return EngUnit(str(other.eng_num / self.eng_num)
213
+ + (other.unit + '/' + self.unit))
214
+
215
+ def __lt__(self, other):
216
+ """
217
+ Compare two engineering numbers, with units
218
+ :param other: EngNum, str, float, or int
219
+ :return: result
220
+ """
221
+ if not isinstance(other, EngNumber):
222
+ other = EngUnit(str(other))
223
+
224
+ if self.unit != other.unit:
225
+ raise AttributeError('units do not match')
226
+
227
+ return self.eng_num < other.eng_num
228
+
229
+ def __gt__(self, other):
230
+ """
231
+ Compare two engineering numbers, with units
232
+ :param other: EngNum, str, float, or int
233
+ :return: result
234
+ """
235
+ if not isinstance(other, EngNumber):
236
+ other = EngUnit(str(other))
237
+
238
+ if self.unit != other.unit:
239
+ raise AttributeError('units do not match')
240
+
241
+ return self.eng_num > other.eng_num
242
+
243
+ def __le__(self, other):
244
+ """
245
+ Compare two engineering numbers, with units
246
+ :param other: EngNum, str, float, or int
247
+ :return: result
248
+ """
249
+ if not isinstance(other, EngNumber):
250
+ other = EngUnit(str(other))
251
+
252
+ if self.unit != other.unit:
253
+ raise AttributeError('units do not match')
254
+
255
+ return self.eng_num <= other.eng_num
256
+
257
+ def __ge__(self, other):
258
+ """
259
+ Compare two engineering numbers, with units
260
+ :param other: EngNum, str, float, or int
261
+ :return: result
262
+ """
263
+ if not isinstance(other, EngNumber):
264
+ other = EngUnit(str(other))
265
+
266
+ if self.unit != other.unit:
267
+ raise AttributeError('units do not match')
268
+
269
+ return self.eng_num >= other.eng_num
270
+
271
+ def __eq__(self, other):
272
+ """
273
+ Compare two engineering numbers, with units
274
+ :param other: EngNum, str, float, or int
275
+ :return: result
276
+ """
277
+ if not isinstance(other, (EngNumber, EngUnit, str, int, float)):
278
+ return NotImplemented
279
+ if not isinstance(other, EngNumber):
280
+ other = EngUnit(str(other))
281
+
282
+ if self.unit != other.unit:
283
+ raise AttributeError('units do not match')
284
+
285
+ return self.eng_num == other.eng_num
286
+
287
+
288
+ class EngNumber:
289
+ """
290
+ Used for easy manipulation of numbers which use engineering notation
291
+ """
292
+
293
+ def __init__(self, value,
294
+ precision=2, significant=0, separator=""):
295
+ """
296
+ Initialize the class
297
+
298
+ :param value: string, integer, or float representing
299
+ the numeric value of the number
300
+ :param precision: the precision past the decimal - default to 2
301
+ :param significant: the number of significant digits
302
+ if given, significant takes precendence over precision
303
+ """
304
+ self.precision = precision
305
+ self.significant = significant
306
+ self.separator = separator
307
+
308
+ if isinstance(value, str):
309
+ suffix_keys = [key for key in _suffix_lookup.keys() if key != '']
310
+
311
+ for suffix in suffix_keys:
312
+ if suffix == value[-1]:
313
+ value = value[:-1] + _suffix_lookup[suffix]
314
+ break
315
+
316
+ self.number = Decimal(value)
317
+
318
+ elif (isinstance(value, int)
319
+ or isinstance(value, float)
320
+ or isinstance(value, EngNumber)):
321
+ self.number = Decimal(str(value))
322
+ else:
323
+ # finally, check for numpy import
324
+ if 'numpy' in sys.modules and isinstance(value, numpy.integer):
325
+ self.number = Decimal(str(value))
326
+
327
+ def to_pn(self, sub_letter=None):
328
+ """
329
+ Returns the part number equivalent. For instance,
330
+ a '1k' would still be '1k', but a
331
+ '1.2k' would, instead, be a '1k2'
332
+ :return:
333
+ """
334
+ string = str(self)
335
+ if '.' not in string:
336
+ return string
337
+
338
+ # take care of the case of when there is no scaling unit
339
+ if not string[-1].isalpha():
340
+ if sub_letter is not None:
341
+ return string.replace('.', sub_letter)
342
+
343
+ return string
344
+
345
+ letter = string[-1]
346
+ return string.replace('.', letter)[:-1].strip(self.separator)
347
+
348
+ def __repr__(self):
349
+ """
350
+ Returns the string representation
351
+ :return: a string representing the engineering number
352
+ """
353
+ # since Decimal class only really converts number that are very small
354
+ # into engineering notation, then we will simply make all number a
355
+ # small number and take advantage of Decimal class
356
+ num_str = self.number * Decimal('10e-31')
357
+ num_str = num_str.to_eng_string().lower()
358
+
359
+ base, exponent = num_str.split('e')
360
+
361
+ if self.significant > 0:
362
+ if abs(Decimal(base)) >= 100.0:
363
+ base = str(round(Decimal(base), self.significant - 3))
364
+ elif abs(Decimal(base)) >= 10.0:
365
+ base = str(round(Decimal(base), self.significant - 2))
366
+ else:
367
+ base = str(round(Decimal(base), self.significant - 1))
368
+ else:
369
+ base = str(round(Decimal(base), self.precision))
370
+
371
+ if 'e' in base.lower():
372
+ base = str(int(Decimal(base)))
373
+
374
+ # remove trailing decimals:
375
+ # print(base)
376
+ # https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python
377
+ # https://stackoverflow.com/questions/11227620/drop-trailing-zeros-from-decimal
378
+ # base = '%s' % float("%#.2G"%Decimal(base))
379
+ # print(base)
380
+ # remove trailing decimal
381
+ if '.' in base:
382
+ base = base.rstrip('.')
383
+
384
+ # remove trailing .00 in precision 2
385
+ if self.precision == 2 and self.significant == 0:
386
+ if '.00' in base:
387
+ base = base[:-3]
388
+
389
+ return base + self.separator + _exponent_lookup_scaled[exponent]
390
+
391
+ def __str__(self, eng=True, context=None):
392
+ """
393
+ Returns the string representation
394
+ :return: a string representing the engineering number
395
+ """
396
+ return self.__repr__()
397
+
398
+ def __int__(self):
399
+ """
400
+ Implements the 'int()' method
401
+ :return:
402
+ """
403
+ return int(self.number)
404
+
405
+ def __float__(self):
406
+ """
407
+ Implements the 'float()' method
408
+ :return:
409
+ """
410
+ return float(self.number)
411
+
412
+ def __add__(self, other):
413
+ """
414
+ Add two engineering numbers
415
+ :param other: EngNum, str, float, or int
416
+ :return: result
417
+ """
418
+ if not isinstance(other, EngNumber):
419
+ other = EngNumber(other)
420
+
421
+ num = self.number + other.number
422
+ return EngNumber(str(num))
423
+
424
+ def __radd__(self, other):
425
+ """
426
+ Add two engineering numbers
427
+ :param other: EngNum, str, float, or int
428
+ :return: result
429
+ """
430
+ return self.__add__(other)
431
+
432
+ def __sub__(self, other):
433
+ """
434
+ Subtract two engineering numbers
435
+ :param other: EngNum, str, float, or int
436
+ :return: result
437
+ """
438
+ if not isinstance(other, EngNumber):
439
+ other = EngNumber(other)
440
+
441
+ num = self.number - other.number
442
+ return EngNumber(str(num))
443
+
444
+ def __rsub__(self, other):
445
+ """
446
+ Subtract two engineering numbers
447
+ :param other: EngNum, str, float, or int
448
+ :return: result
449
+ """
450
+ if not isinstance(other, EngNumber):
451
+ other = EngNumber(other)
452
+
453
+ num = other.number - self.number
454
+ return EngNumber(str(num))
455
+
456
+ def __mul__(self, other):
457
+ """
458
+ Multiply two engineering numbers
459
+ :param other: EngNum, str, float, or int
460
+ :return: result
461
+ """
462
+ if not isinstance(other, EngNumber):
463
+ other = EngNumber(other)
464
+
465
+ num = self.number * other.number
466
+ return EngNumber(str(num))
467
+
468
+ def __rmul__(self, other):
469
+ """
470
+ Multiply two engineering numbers
471
+ :param other: EngNum, str, float, or int
472
+ :return: result
473
+ """
474
+ return self.__mul__(other)
475
+
476
+ def __truediv__(self, other):
477
+ """
478
+ Divide two engineering numbers
479
+ :param other: EngNum, str, float, or int
480
+ :return: result
481
+ """
482
+ if not isinstance(other, EngNumber):
483
+ other = EngNumber(other)
484
+
485
+ num = self.number / other.number
486
+ return EngNumber(str(num))
487
+
488
+ def __rtruediv__(self, other):
489
+ """
490
+ Divide two engineering numbers
491
+ :param other: EngNum, str, float, or int
492
+ :return: result
493
+ """
494
+ if not isinstance(other, EngNumber):
495
+ other = EngNumber(other)
496
+
497
+ num = other.number / self.number
498
+ return EngNumber(str(num))
499
+
500
+ def __lt__(self, other):
501
+ """
502
+ Compare two engineering numbers
503
+ :param other: EngNum, str, float, or int
504
+ :return: result
505
+ """
506
+ if not isinstance(other, EngNumber):
507
+ other = EngNumber(other)
508
+
509
+ return self.number < other.number
510
+
511
+ def __gt__(self, other):
512
+ """
513
+ Compare two engineering numbers
514
+ :param other: EngNum, str, float, or int
515
+ :return: result
516
+ """
517
+ if not isinstance(other, EngNumber):
518
+ other = EngNumber(other)
519
+
520
+ return self.number > other.number
521
+
522
+ def __le__(self, other):
523
+ """
524
+ Compare two engineering numbers
525
+ :param other: EngNum, str, float, or int
526
+ :return: result
527
+ """
528
+ if not isinstance(other, EngNumber):
529
+ other = EngNumber(other)
530
+
531
+ return self.number <= other.number
532
+
533
+ def __ge__(self, other):
534
+ """
535
+ Compare two engineering numbers
536
+ :param other: EngNum, str, float, or int
537
+ :return: result
538
+ """
539
+ if not isinstance(other, EngNumber):
540
+ other = EngNumber(other)
541
+
542
+ return self.number >= other.number
543
+
544
+ def __eq__(self, other):
545
+ """
546
+ Compare two engineering numbers
547
+ :param other: EngNum, str, float, or int
548
+ :return: result
549
+ """
550
+ if not isinstance(other, (EngNumber, str, int, float)):
551
+ return NotImplemented
552
+ if not isinstance(other, EngNumber):
553
+ other = EngNumber(other)
554
+
555
+ return self.number == other.number
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version("engineering_notation")