commodutil 0.0.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.
Files changed (31) hide show
  1. commodutil-0.0.0/PKG-INFO +17 -0
  2. commodutil-0.0.0/commodutil/__init__.py +0 -0
  3. commodutil-0.0.0/commodutil/arb.py +14 -0
  4. commodutil-0.0.0/commodutil/convfactors.py +474 -0
  5. commodutil-0.0.0/commodutil/dates.py +57 -0
  6. commodutil-0.0.0/commodutil/forward/__init__.py +0 -0
  7. commodutil-0.0.0/commodutil/forward/calendar.py +279 -0
  8. commodutil-0.0.0/commodutil/forward/continuous.py +106 -0
  9. commodutil-0.0.0/commodutil/forward/fly.py +82 -0
  10. commodutil-0.0.0/commodutil/forward/quarterly.py +200 -0
  11. commodutil-0.0.0/commodutil/forward/spreads.py +104 -0
  12. commodutil-0.0.0/commodutil/forward/structure.py +32 -0
  13. commodutil-0.0.0/commodutil/forward/util.py +91 -0
  14. commodutil-0.0.0/commodutil/forwards.py +355 -0
  15. commodutil-0.0.0/commodutil/pandasutil.py +118 -0
  16. commodutil-0.0.0/commodutil/stats.py +50 -0
  17. commodutil-0.0.0/commodutil/transforms.py +233 -0
  18. commodutil-0.0.0/commodutil.egg-info/PKG-INFO +17 -0
  19. commodutil-0.0.0/commodutil.egg-info/SOURCES.txt +29 -0
  20. commodutil-0.0.0/commodutil.egg-info/dependency_links.txt +1 -0
  21. commodutil-0.0.0/commodutil.egg-info/requires.txt +7 -0
  22. commodutil-0.0.0/commodutil.egg-info/top_level.txt +1 -0
  23. commodutil-0.0.0/pyproject.toml +37 -0
  24. commodutil-0.0.0/setup.cfg +4 -0
  25. commodutil-0.0.0/tests/test_arb.py +25 -0
  26. commodutil-0.0.0/tests/test_conv.py +335 -0
  27. commodutil-0.0.0/tests/test_dates.py +34 -0
  28. commodutil-0.0.0/tests/test_forwards.py +140 -0
  29. commodutil-0.0.0/tests/test_pandasutils.py +58 -0
  30. commodutil-0.0.0/tests/test_stats.py +57 -0
  31. commodutil-0.0.0/tests/test_transforms.py +98 -0
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: commodutil
3
+ Version: 0.0.0
4
+ Summary: common commodity/oil analytics utils
5
+ Author-email: aeorxc <author@example.com>
6
+ Project-URL: Homepage, https://github.com/aeorxc/commodutil
7
+ Project-URL: Source, https://github.com/aeorxc/commodutil
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: pandas
13
+ Requires-Dist: dask[delayed]
14
+ Requires-Dist: pint
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
17
+ Requires-Dist: pytest-runner; extra == "test"
File without changes
@@ -0,0 +1,14 @@
1
+ import pandas as pd
2
+ from commodutil.pandasutil import apply_formula
3
+
4
+
5
+ def calc(df: pd.DataFrame, dest: str, orig: str, freight: str = None) -> pd.DataFrame:
6
+ """Calculate an arb by pulling all load and discharge symbols and subtract freight
7
+ """
8
+
9
+ formula = f"{dest} - {orig}"
10
+ if freight is not None:
11
+ formula = f"{formula} - {freight}"
12
+
13
+ df = apply_formula(df, formula)
14
+ return df
@@ -0,0 +1,474 @@
1
+ """
2
+ Modern implementation of commodity unit conversions using Pint.
3
+ Clean-slate design with no backward-compatibility constraints.
4
+ """
5
+
6
+ import pint
7
+ from pint.errors import DimensionalityError
8
+ from typing import Union, Optional
9
+ from dataclasses import dataclass
10
+ import pandas as pd
11
+ from functools import lru_cache
12
+
13
+ # Initialize pint with custom definitions
14
+ ureg = pint.UnitRegistry()
15
+
16
+ # Define oil & gas specific units
17
+ ureg.define('barrel = 158.987294928 liter = bbl')
18
+ ureg.define('gallon = 3.785411784 liter = gal')
19
+ ureg.define('metric_ton = 1000 kilogram = mt')
20
+ ureg.define('kiloton = 1000 metric_ton = kt')
21
+ ureg.define('cubic_kilometer = 1e9 meter**3 = km3') # 1 km^3 = 1 billion m^3
22
+ ureg.define('gigajoule = 1e9 joule = gj = GJ')
23
+ ureg.define('petajoule = 1e15 joule = pj = PJ')
24
+ ureg.define('billion_cubic_meter = 1e9 meter**3 = bcm = BCM')
25
+ ureg.define('billion_cubic_foot = 1e9 foot**3 = bcf = BCF')
26
+ ureg.define('tonne_of_oil_equivalent = 41.868e9 joule = toe = TOE')
27
+ ureg.define('million_tonne_of_oil_equivalent = 1e6 tonne_of_oil_equivalent = Mtoe')
28
+ ureg.define('barrel_of_oil_equivalent = 6.119e9 joule = boe = BOE')
29
+ ureg.define('million_barrel_of_oil_equivalent = 1e6 barrel_of_oil_equivalent = Mboe')
30
+ ureg.define('megatonne = 1e6 metric_ton = Mt')
31
+
32
+ @dataclass
33
+ class Commodity:
34
+ """Represents a commodity with its physical properties"""
35
+ name: str
36
+ density: pint.Quantity # kg/L or API gravity
37
+ energy_content: Optional[pint.Quantity] = None # GJ/m^3 or similar
38
+
39
+ def __post_init__(self):
40
+ # Ensure quantities have correct dimensions
41
+ if not isinstance(self.density, pint.Quantity):
42
+ self.density = self.density * ureg.kg / ureg.liter
43
+ if self.energy_content and not isinstance(self.energy_content, pint.Quantity):
44
+ self.energy_content = self.energy_content * ureg.GJ / ureg.m**3
45
+
46
+ # Define commodities with their properties and correct industry factors
47
+ COMMODITIES = {
48
+ # Crude oil (BP approximate conversion factors)
49
+ # 1 mt ≈ 7.33 bbl and ≈ 1.165 kL => density ≈ 0.85809 kg/L
50
+ 'crude': Commodity('crude', 0.85809151 * ureg.kg/ureg.L, None),
51
+
52
+ # Light ends - tuned to match kbbl/kt figures exactly
53
+ 'gasoline': Commodity('gasoline', 0.755079324 * ureg.kg/ureg.L, 33.7898 * ureg.GJ/ureg.m**3), # BP: 44.75 GJ/t
54
+ 'naphtha': Commodity('naphtha', 0.706720311 * ureg.kg/ureg.L, None), # 8.90 kbbl/kt
55
+ 'ethanol': Commodity('ethanol', 0.755079324 * ureg.kg/ureg.L, 21 * ureg.GJ/ureg.m**3), # 8.33 kbbl/kt
56
+
57
+ # Middle distillates
58
+ 'diesel': Commodity('diesel', 0.844269902 * ureg.kg/ureg.L, 36.624428 * ureg.GJ/ureg.m**3), # BP: 43.38 GJ/t
59
+ 'jet': Commodity('jet', 0.798199336 * ureg.kg/ureg.L, 35.056915 * ureg.GJ/ureg.m**3), # BP: 43.92 GJ/t
60
+ 'fame': Commodity('fame', 0.892001564 * ureg.kg/ureg.L, 33 * ureg.GJ/ureg.m**3), # 7.051345 kbbl/kt
61
+ 'hvo': Commodity('hvo', 0.781731391 * ureg.kg/ureg.L, 34 * ureg.GJ/ureg.m**3), # 8.046 kbbl/kt
62
+
63
+ # Heavy products
64
+ 'vgo': Commodity('vgo', 0.911566778 * ureg.kg/ureg.L, None), # 6.90 kbbl/kt
65
+ 'fuel_oil': Commodity('fuel_oil', 0.990521381 * ureg.kg/ureg.L, 41.175974 * ureg.GJ/ureg.m**3), # BP: 41.57 GJ/t
66
+
67
+ # LPG and Natural gas (liquefied)
68
+ 'lpg': Commodity('lpg', 0.541 * ureg.kg/ureg.L, 24.96715 * ureg.GJ/ureg.m**3), # BP: LPG 46.15 GJ/t
69
+ 'natgas': Commodity('natgas', 0.542225066 * ureg.kg/ureg.L, 26.137 * ureg.GJ/ureg.m**3),
70
+
71
+ # Natural gas (gaseous, pipeline): BP approx 36 PJ per bcm => 0.036 GJ/m**3
72
+ 'natural_gas': Commodity('natural_gas', 0.0 * ureg.kg/ureg.L, 0.036 * ureg.GJ/ureg.m**3), # LNG figures
73
+
74
+ # Light gases
75
+ 'ethane': Commodity('ethane', 0.373 * ureg.kg/ureg.L, 18.4262 * ureg.GJ/ureg.m**3), # BP: 49.4 GJ/t
76
+
77
+ # BP product basket (optional reference)
78
+ 'product_basket': Commodity('product_basket', 0.781 * ureg.kg/ureg.L, 33.642356 * ureg.GJ/ureg.m**3),
79
+ }
80
+
81
+ # Aliases for compatibility
82
+ ALIASES = {
83
+ # Common synonyms and marketing terms
84
+ 'ulsd': 'diesel',
85
+ 'gasoil': 'diesel',
86
+ 'gas_oil': 'diesel',
87
+ 'gas oil': 'diesel',
88
+ 'go': 'diesel',
89
+ 'kerosene': 'jet',
90
+
91
+ # Motor gasoline
92
+ 'gas': 'gasoline',
93
+ 'mogas': 'gasoline',
94
+
95
+ # Fuel oil
96
+ 'fueloil': 'fuel_oil',
97
+ 'fuel oil': 'fuel_oil',
98
+ 'fo': 'fuel_oil',
99
+
100
+ # Crude
101
+ 'crude oil': 'crude',
102
+ 'crudeoil': 'crude',
103
+
104
+ # LPG and nat gas
105
+ 'propane': 'lpg',
106
+ 'lng': 'natgas', # LNG (liquefied natural gas)
107
+ 'ng': 'natural_gas', # pipeline natural gas (gaseous)
108
+ 'naturalgas': 'natural_gas',
109
+ 'nat_gas': 'natural_gas',
110
+ }
111
+
112
+ class CommodityConverter:
113
+ """Clean, modern interface for commodity unit conversions"""
114
+
115
+ def __init__(self):
116
+ self.ureg = ureg
117
+ self.commodities = COMMODITIES
118
+ self.aliases = ALIASES
119
+
120
+ @lru_cache(maxsize=128)
121
+ def get_commodity(self, name: str) -> Commodity:
122
+ """Get commodity object, resolving aliases"""
123
+ name = name.lower()
124
+ name = self.aliases.get(name, name)
125
+ if name not in self.commodities:
126
+ raise ValueError(f"Unknown commodity: {name}")
127
+ return self.commodities[name]
128
+
129
+ def convert(self,
130
+ value: Union[float, pd.Series],
131
+ from_unit: str,
132
+ to_unit: str,
133
+ commodity: Optional[str] = None) -> Union[float, pd.Series]:
134
+ """
135
+ Convert between units, using commodity properties when needed
136
+
137
+ Examples:
138
+ # Simple unit conversion (no commodity needed)
139
+ convert(100, 'bbl', 'L')
140
+
141
+ # Mass to volume (needs commodity density)
142
+ convert(100, 'kt', 'bbl', commodity='diesel')
143
+
144
+ # Energy conversions
145
+ convert(1000, 'm^3', 'GJ', commodity='diesel')
146
+
147
+ # With pandas Series and daily rates
148
+ convert(series, 'kt/month', 'bbl/day', commodity='gasoline')
149
+ """
150
+ # Normalize and parse units to handle daily/monthly rates
151
+ from_unit = self._normalize_unit(from_unit)
152
+ to_unit = self._normalize_unit(to_unit)
153
+ from_rate = self._parse_rate_unit(from_unit)
154
+ to_rate = self._parse_rate_unit(to_unit)
155
+
156
+ # Get base units
157
+ from_base = from_rate['base']
158
+ to_base = to_rate['base']
159
+
160
+ # Create quantity
161
+ if isinstance(value, pd.Series):
162
+ result = self._convert_series(value, from_base, to_base, commodity,
163
+ from_rate['period'], to_rate['period'])
164
+ else:
165
+ result = self._convert_scalar(value, from_base, to_base, commodity)
166
+ if from_rate['period'] or to_rate['period']:
167
+ factor = self._rate_factor_scalar(from_rate['period'], to_rate['period'])
168
+ result = result * factor
169
+
170
+ return result
171
+
172
+ def _convert_scalar(self, value: float, from_unit: str, to_unit: str,
173
+ commodity: Optional[str]) -> float:
174
+ """Convert a scalar value across mass/volume/energy using commodity context when needed."""
175
+ from_unit = self._normalize_unit(from_unit)
176
+ to_unit = self._normalize_unit(to_unit)
177
+ qty = value * self.ureg(from_unit)
178
+
179
+ # Try direct conversion first
180
+ try:
181
+ return qty.to(to_unit).magnitude
182
+ except DimensionalityError:
183
+ pass
184
+
185
+ # Determine unit types
186
+ is_from_energy = self._is_energy(from_unit)
187
+ is_to_energy = self._is_energy(to_unit)
188
+ is_from_mass = self._is_mass(from_unit)
189
+ is_to_mass = self._is_mass(to_unit)
190
+ is_from_volume = self._is_volume(from_unit)
191
+ is_to_volume = self._is_volume(to_unit)
192
+
193
+ # Energy conversions
194
+ if is_from_energy or is_to_energy:
195
+ if not commodity:
196
+ raise ValueError("Commodity required for energy conversion")
197
+ comm = self.get_commodity(commodity)
198
+ if not comm.energy_content:
199
+ raise ValueError(f"No energy content defined for {commodity}")
200
+
201
+ ec = comm.energy_content.to('J/m^3')
202
+
203
+ if is_from_energy:
204
+ energy_J = qty.to('J')
205
+ # Energy -> Volume or Mass
206
+ volume_m3 = (energy_J / ec).to('m^3')
207
+ if is_to_volume:
208
+ return volume_m3.to(to_unit).magnitude
209
+ elif is_to_mass:
210
+ density_kg_m3 = comm.density.to('kg/m^3')
211
+ if density_kg_m3.magnitude == 0:
212
+ raise ValueError(f"Density not defined for {commodity}; cannot convert energy to mass")
213
+ mass_kg = (volume_m3 * density_kg_m3).to('kg')
214
+ return mass_kg.to(to_unit).magnitude
215
+ else:
216
+ raise ValueError(f"Cannot convert energy to {to_unit}")
217
+ else:
218
+ # Volume/Mass -> Energy
219
+ if is_from_mass:
220
+ density_kg_m3 = comm.density.to('kg/m^3')
221
+ if density_kg_m3.magnitude == 0:
222
+ raise ValueError(f"Density not defined for {commodity}; cannot convert mass to energy")
223
+ mass_kg = qty.to('kg')
224
+ volume_m3 = (mass_kg / density_kg_m3).to('m^3')
225
+ elif is_from_volume:
226
+ volume_m3 = qty.to('m^3')
227
+ else:
228
+ raise ValueError(f"Cannot convert {from_unit} to energy")
229
+ energy_J = (volume_m3 * ec).to('J')
230
+ return energy_J.to(to_unit).magnitude
231
+
232
+ # Mass <-> Volume conversions require density
233
+ if (is_from_mass and is_to_volume) or (is_from_volume and is_to_mass):
234
+ if not commodity:
235
+ raise ValueError(f"Commodity required for {from_unit} to {to_unit}")
236
+ comm = self.get_commodity(commodity)
237
+ density_kg_L = comm.density.to('kg/L')
238
+ density_kg_m3 = comm.density.to('kg/m^3')
239
+ if density_kg_L.magnitude == 0 or density_kg_m3.magnitude == 0:
240
+ raise ValueError(f"Density not defined for {commodity}; cannot convert between mass and volume")
241
+ if is_from_mass and is_to_volume:
242
+ mass_kg = qty.to('kg')
243
+ volume_L = (mass_kg / density_kg_L).to('L')
244
+ return volume_L.to(to_unit).magnitude
245
+ else:
246
+ volume_L = qty.to('L')
247
+ mass_kg = (volume_L * density_kg_L).to('kg')
248
+ return mass_kg.to(to_unit).magnitude
249
+
250
+ raise ValueError(f"Cannot convert from {from_unit} to {to_unit} - incompatible dimensions")
251
+
252
+ def _convert_series(self, series: pd.Series, from_unit: str, to_unit: str,
253
+ commodity: Optional[str], from_period: Optional[str],
254
+ to_period: Optional[str]) -> pd.Series:
255
+ """Convert a pandas Series with optional rate handling.
256
+
257
+ - Month conversions use the index's actual days_in_month when available.
258
+ - Other time conversions use standard averages (365.25 days/year, 30.4375 days/month).
259
+ """
260
+ result = series.copy()
261
+
262
+ # Handle period conversions for rates
263
+ if from_period or to_period:
264
+ if from_period != to_period:
265
+ if hasattr(series.index, 'days_in_month') and (
266
+ (from_period == 'day' and to_period == 'month') or
267
+ (from_period == 'month' and to_period == 'day')
268
+ ):
269
+ # Month-aware conversions using calendar days per month
270
+ if from_period == 'day' and to_period == 'month':
271
+ result = result * series.index.days_in_month
272
+ else:
273
+ result = result / series.index.days_in_month
274
+ else:
275
+ # Fallback to scalar factor for other period conversions
276
+ factor = self._rate_factor_scalar(from_period, to_period)
277
+ result = result * factor
278
+
279
+ # Apply unit conversion
280
+ factor_units = self._convert_scalar(1.0, from_unit, to_unit, commodity)
281
+ result = result * factor_units
282
+
283
+ return result
284
+
285
+ def _parse_rate_unit(self, unit: str) -> dict:
286
+ """Parse units like 'bbl/day' or 'kt/month'."""
287
+ if '/' in unit:
288
+ base, period = unit.split('/', 1)
289
+ base = self._normalize_unit(base)
290
+ period = period.strip().lower().rstrip('s') # day(s), month(s), year(s)
291
+ return {'base': base, 'period': period}
292
+ return {'base': self._normalize_unit(unit), 'period': None}
293
+
294
+ def _rate_factor_scalar(self, from_period: Optional[str], to_period: Optional[str]) -> float:
295
+ """Scalar factor to convert between rate periods for scalars.
296
+
297
+ Uses average calendar lengths when months/years are involved.
298
+ - Average days per year: 365.25
299
+ - Average days per month: 365.25 / 12 = 30.4375
300
+ """
301
+ if from_period == to_period:
302
+ return 1.0
303
+ if from_period is None and to_period is None:
304
+ return 1.0
305
+ if from_period is None or to_period is None:
306
+ # Ambiguous to add/remove a time dimension for scalar; no-op to preserve behavior
307
+ return 1.0
308
+
309
+ avg_days_per_year = 365.25
310
+ avg_days_per_month = avg_days_per_year / 12.0
311
+
312
+ # Helper to express rates as per-day factors
313
+ def per_day_factor(period: str) -> float:
314
+ if period == 'day':
315
+ return 1.0
316
+ if period == 'year':
317
+ return 1.0 / avg_days_per_year
318
+ if period == 'month':
319
+ return 1.0 / avg_days_per_month
320
+ # Fallback to Pint if it's a known time unit
321
+ try:
322
+ return (1 * (self.ureg(period) ** -1)).to(self.ureg.day ** -1).magnitude
323
+ except Exception:
324
+ raise ValueError(f"Unsupported rate period: {period}")
325
+
326
+ from_per_day = per_day_factor(from_period)
327
+ to_per_day = per_day_factor(to_period)
328
+ # Convert from per-from_period to per-to_period
329
+ return from_per_day / to_per_day
330
+
331
+ def _is_energy(self, unit: str) -> bool:
332
+ try:
333
+ (1 * self.ureg(unit)).to('J')
334
+ return True
335
+ except DimensionalityError:
336
+ return False
337
+
338
+ def _is_mass(self, unit: str) -> bool:
339
+ try:
340
+ (1 * self.ureg(unit)).to('kg')
341
+ return True
342
+ except DimensionalityError:
343
+ return False
344
+
345
+ def _is_volume(self, unit: str) -> bool:
346
+ try:
347
+ (1 * self.ureg(unit)).to('m^3')
348
+ return True
349
+ except DimensionalityError:
350
+ return False
351
+
352
+ def _normalize_unit(self, unit: str) -> str:
353
+ """Normalize common aliases and fix encoding issues.
354
+
355
+ - Map 'm��'/'m³'/'m**3'/'cubic_meter' -> 'm^3'
356
+ - Map energy aliases 'BTU' -> 'Btu', 'MMBTU' -> 'MMBtu'
357
+ - Trim whitespace
358
+ """
359
+ if unit is None:
360
+ return unit
361
+ u = unit.strip()
362
+ # Fix cubic meter notations and encoding issues
363
+ replacements = {
364
+ 'm��': 'm^3',
365
+ 'm³': 'm^3',
366
+ 'm**3': 'm^3',
367
+ 'cubic_meter': 'm^3',
368
+ 'CUBIC_METER': 'm^3',
369
+ }
370
+ for bad, good in replacements.items():
371
+ u = u.replace(bad, good)
372
+
373
+ # Additional robust normalizations using ASCII-only fallbacks
374
+ if u.lower() == 'm3':
375
+ u = 'm^3'
376
+ # Handle rate-style variants like 'm3/day' or 'M3/day'
377
+ u = u.replace('m3/', 'm^3/').replace('M3/', 'm^3/')
378
+ # Energy unit common uppercase forms
379
+ if u == 'BTU':
380
+ u = 'Btu'
381
+ if u == 'MMBTU':
382
+ u = 'MMBtu'
383
+ return u
384
+
385
+ @property
386
+ def available_commodities(self) -> list:
387
+ """List all available commodities"""
388
+ return list(self.commodities.keys())
389
+
390
+ @property
391
+ def available_units(self) -> list:
392
+ """List common units for oil & gas"""
393
+ return [
394
+ # Volume
395
+ 'bbl', 'barrel', 'L', 'liter', 'm³', 'cubic_meter', 'gal', 'gallon', 'bcm', 'bcf',
396
+ # Mass
397
+ 'kg', 'mt', 'metric_ton', 'kt', 'kiloton', 't', 'tonne', 'Mt',
398
+ # Energy
399
+ 'J', 'GJ', 'gigajoule', 'MJ', 'megajoule', 'PJ', 'toe', 'Mtoe', 'boe', 'Mboe', 'BTU', 'MMBTU',
400
+ # Rates
401
+ 'bbl/day', 'kt/month', 'm³/day', 'mt/year'
402
+ ]
403
+
404
+ # Global converter instance for convenience
405
+ converter = CommodityConverter()
406
+
407
+ # Convenience functions for direct use
408
+ def convert(value, from_unit: str, to_unit: str, commodity: Optional[str] = None):
409
+ """Convert values between units"""
410
+ return converter.convert(value, from_unit, to_unit, commodity)
411
+
412
+ def convfactor(from_unit: str, to_unit: str, commodity: Optional[str] = None) -> float:
413
+ """Get conversion factor between units"""
414
+ return converter.convert(1.0, from_unit, to_unit, commodity)
415
+
416
+ def list_commodities():
417
+ """List all available commodities"""
418
+ return converter.available_commodities
419
+
420
+ def list_units():
421
+ """List common units"""
422
+ # Return normalized forms to avoid encoding issues
423
+ return [converter._normalize_unit(u) for u in converter.available_units]
424
+
425
+ # Example usage
426
+ if __name__ == "__main__":
427
+ print("Modern Commodity Converter Examples\n" + "="*50)
428
+
429
+ # Simple conversions
430
+ print("\n1. Simple unit conversions (no commodity needed):")
431
+ print(f"100 bbl = {convert(100, 'bbl', 'L'):.0f} L")
432
+ print(f"1000 L = {convert(1000, 'L', 'bbl'):.2f} bbl")
433
+
434
+ # Commodity-specific conversions
435
+ print("\n2. Mass-Volume conversions (needs commodity):")
436
+ print(f"100 kt diesel = {convert(100, 'kt', 'bbl', 'diesel'):.0f} bbl")
437
+ print(f"1000 bbl gasoline = {convert(1000, 'bbl', 'mt', 'gasoline'):.2f} mt")
438
+
439
+ # Energy conversions (now implemented across mass/volume/energy)
440
+ print("\n3. Energy conversions:")
441
+ print("(Energy conversions implemented for mass/volume/energy)")
442
+
443
+ # Series with daily rates
444
+ print("\n4. Pandas Series with rate conversions:")
445
+ dates = pd.date_range('2024-01', periods=3, freq='MS')
446
+ series = pd.Series([100, 110, 105], index=dates)
447
+ result = convert(series, 'kt/month', 'bbl/day', 'diesel')
448
+ print(f"January: {result.iloc[0]:.0f} bbl/day")
449
+
450
+ # Available commodities
451
+ print(f"\n5. Available commodities: {', '.join(list_commodities())}")
452
+
453
+ # Error handling
454
+ print("\n6. Error handling:")
455
+ try:
456
+ convert(100, 'kt', 'bbl') # Missing commodity
457
+ except ValueError as e:
458
+ print(f"Error: {e}")
459
+
460
+ print("\n" + "="*50)
461
+ print("Key improvements over original:")
462
+ print("• Type hints and dataclasses for clarity")
463
+ print("• Automatic dimensional analysis")
464
+ print("• Clean separation of concerns")
465
+ print("• Caching for performance")
466
+ print("• Better error messages")
467
+ print("• Extensible commodity definitions")
468
+ print("• Modern Python patterns")
469
+
470
+
471
+
472
+
473
+
474
+
@@ -0,0 +1,57 @@
1
+ import datetime
2
+ import re
3
+ import time
4
+ from datetime import datetime, date, timedelta
5
+
6
+ curmon = datetime.now().month
7
+ curyear = datetime.now().year
8
+ curmonyear = datetime(curyear, curmon, 1)
9
+ curmonyear_str = "%s-%s" % (curyear, curmon) # get pandas time filtering
10
+
11
+ last_day_of_prev_month = date.today().replace(day=1) - timedelta(days=1)
12
+ start_day_of_prev_month = date.today().replace(day=1) - timedelta(
13
+ days=last_day_of_prev_month.day
14
+ )
15
+
16
+ prevmon = start_day_of_prev_month.month
17
+ prevmon_str = "%s-%s" % (
18
+ start_day_of_prev_month.year,
19
+ start_day_of_prev_month.month,
20
+ ) # get pandas time filtering
21
+
22
+ nextyear = curyear + 1
23
+ prevyear = curyear - 1
24
+
25
+
26
+ def find_year(df, use_delta=False):
27
+ """
28
+ Given a dataframe find the years in the column headings. Return a dict of colname to year
29
+ eg { 'Q1 2016' : 2016, 'Q1 2017' : 2017
30
+ """
31
+ res = {}
32
+ for colname in df:
33
+ colregex = re.findall("\d\d\d\d", str(colname))
34
+ colyear = None
35
+ if len(colregex) >= 1:
36
+ colyear = int(colregex[0])
37
+
38
+ if colyear:
39
+ res[colname] = colyear
40
+ if colyear and use_delta:
41
+ delta = colyear - curyear
42
+ res[colname] = delta
43
+ else:
44
+ res[colname] = colname
45
+
46
+ return res
47
+
48
+
49
+ def time_until_end_of_day(dt=None):
50
+ # type: (datetime.datetime) -> datetime.timedelta
51
+ """
52
+ Get timedelta until end of day on the datetime passed, or current time.
53
+ """
54
+ if dt is None:
55
+ dt = datetime.now()
56
+ tomorrow = dt + timedelta(days=1)
57
+ return (datetime.combine(tomorrow, time.min) - dt).seconds
File without changes