@bereasoftware/time-guard 2.0.8 → 2.2.0

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.
package/README.md CHANGED
@@ -232,6 +232,120 @@ console.log(date.inLeapYear()); // true
232
232
 
233
233
  ## ⏱️ Cálculos Avanzados
234
234
 
235
+ ### 🎯 API Semántica: `between()` - Sin Carga Mental
236
+
237
+ La forma **más semántica y clara** para calcular diferencias de tiempo. ¡No necesitas pensar en `until()` vs `since()`!
238
+
239
+ ```typescript
240
+ const start = TimeGuard.from("2024-01-15");
241
+ const end = TimeGuard.from("2024-03-20");
242
+
243
+ // Lectura simple y directa
244
+ TimeGuard.between(start, end).humanize();
245
+ // "2 months and 5 days"
246
+
247
+ // El orden NO importa - siempre positivo
248
+ TimeGuard.between(end, start).humanize();
249
+ // "2 months and 5 days" (¡idéntico!)
250
+
251
+ // Acceso a propiedades DurationParts
252
+ TimeGuard.between(start, end).months; // 2
253
+ TimeGuard.between(start, end).days; // 5
254
+
255
+ // Todas las opciones de humanize() disponibles
256
+ TimeGuard.between(start, end).humanize({ locale: "es", fullBreakdown: true });
257
+ // "2 meses y 5 días"
258
+ ```
259
+
260
+ **Comparación semántica:**
261
+
262
+ ```typescript
263
+ // ❌ Confuso: ¿cuál es cuál?
264
+ start.until(end); // ¿es positivo?
265
+ end.since(start); // ¿es diferente?
266
+
267
+ // ✅ Claro: no necesitas pensar
268
+ TimeGuard.between(start, end); // siempre positivo, sin confusión
269
+ TimeGuard.between(end, start); // mismo resultado, order no importa
270
+ ```
271
+
272
+ ### 🔗 API Fluente: `range()` - Naming Killer
273
+
274
+ **Marketing técnico puro:** La API más intuitiva para rangos de fechas con **method chaining fluent**:
275
+
276
+ ```typescript
277
+ // Sintaxis limpia y directa
278
+ TimeGuard.range("2024-01-15", "2024-03-20").humanize();
279
+ // "2 months and 5 days"
280
+
281
+ TimeGuard.range("2024-01-15", "2024-03-20").inMonths();
282
+ // 2.1355 (valor decimal preciso)
283
+
284
+ TimeGuard.range("2024-01-15", "2024-03-20").toDuration();
285
+ // DurationResult: acceso completo a propiedades
286
+ ```
287
+
288
+ **Casos de uso empresariales:**
289
+
290
+ ```typescript
291
+ // 📅 Período de alquiler
292
+ const checkIn = new TimeGuard("2024-06-15");
293
+ const checkOut = new TimeGuard("2024-06-22");
294
+
295
+ const rentalDays = TimeGuard.range(checkIn, checkOut).in("day");
296
+ const rentalCost = rentalDays * 100; // $100 por día
297
+ console.log(rentalCost); // $700
298
+
299
+ // 💳 Cálculo de mora
300
+ const invoiceDate = new TimeGuard("2024-01-01");
301
+ const paymentDate = new TimeGuard("2024-02-15");
302
+
303
+ const lateFeePerDay = 10;
304
+ const lateFee =
305
+ TimeGuard.range(invoiceDate, paymentDate).in("day") * lateFeePerDay;
306
+ console.log(lateFee); // Cálculo exacto con promediado de meses
307
+
308
+ // 📊 Analítica de sesiones
309
+ const sessionStart = new TimeGuard("2024-03-15T10:00:00");
310
+ const sessionEnd = new TimeGuard("2024-03-15T10:35:42");
311
+
312
+ const engagementMinutes = TimeGuard.range(sessionStart, sessionEnd).in(
313
+ "minute",
314
+ );
315
+ console.log(engagementMinutes); // Para métricas de usuarios
316
+ ```
317
+
318
+ **Soporte de Locales:**
319
+
320
+ ```typescript
321
+ // El orden de fechas NO importa
322
+ TimeGuard.range("2024-03-20", "2024-01-15").humanize({ locale: "es" });
323
+ // "2 meses y 5 días" (resultado idéntico)
324
+
325
+ // Diferentes idiomas
326
+ TimeGuard.range("2024-01-15", "2024-03-20").humanize({ locale: "fr" });
327
+ // "2 mois et 5 jours"
328
+
329
+ TimeGuard.range("2024-01-15", "2024-03-20").humanize({ locale: "de" });
330
+ // "2 Monate und 5 Tage"
331
+
332
+ // Desglose completo
333
+ TimeGuard.range("2024-01-15", "2024-03-20").humanize({
334
+ locale: "es",
335
+ fullBreakdown: true,
336
+ });
337
+ // "2 meses y 5 días"
338
+ ```
339
+
340
+ **Métodos disponibles en `TimeRange`:**
341
+
342
+ | Método | Retorna | Descripción |
343
+ | --------------------- | ---------------- | --------------------------------------------------- |
344
+ | `.toDuration()` | `DurationResult` | Objeto de duración completo con todos los métodos |
345
+ | `.inMonths()` | `number` | Rango en meses (decimal: 2.1355) |
346
+ | `.humanize(options?)` | `string` | Texto legible: "2 meses y 5 días" |
347
+ | `.in(unit)` | `number` | Rango en cualquier unidad (day, hour, minute, etc.) |
348
+
235
349
  ### Duración: Calcular tiempo entre fechas
236
350
 
237
351
  ```typescript
@@ -252,7 +366,185 @@ console.log(duration);
252
366
  // }
253
367
  ```
254
368
 
255
- ### Redondeo: Control de precisión
369
+ ### Humanize: Duración Legible para Usuarios 📝
370
+
371
+ Convierte duraciones a texto natural y legible con soporte multiidioma:
372
+
373
+ ```typescript
374
+ const start = TimeGuard.from("2024-01-15");
375
+ const end = TimeGuard.from("2024-03-20");
376
+
377
+ // Estilo simple (Intl.RelativeTimeFormat)
378
+ const duration = start.until(end);
379
+ console.log(duration.humanize());
380
+ // "2 months"
381
+
382
+ // Desglose completo con múltiples unidades
383
+ console.log(duration.humanize({ fullBreakdown: true }));
384
+ // "2 months and 5 days"
385
+
386
+ // Con locale específico
387
+ console.log(duration.humanize({ locale: "es" }));
388
+ // "2 meses"
389
+
390
+ console.log(duration.humanize({ locale: "es", fullBreakdown: true }));
391
+ // "2 meses y 5 días"
392
+
393
+ // Con locale francés
394
+ console.log(duration.humanize({ locale: "fr" }));
395
+ // "2 mois"
396
+
397
+ console.log(duration.humanize({ locale: "fr", fullBreakdown: true }));
398
+ // "2 mois et 5 jours"
399
+ ```
400
+
401
+ #### Uso de Locales Heredadas
402
+
403
+ ```typescript
404
+ // TimeGuard hereda configuración de locale
405
+ const start = TimeGuard.from("2024-01-15", { locale: "es" });
406
+ const end = TimeGuard.from("2024-03-20");
407
+
408
+ const duration = start.until(end);
409
+
410
+ // Usa automáticamente el locale de la instancia
411
+ console.log(duration.humanize());
412
+ // "2 meses"
413
+
414
+ console.log(duration.humanize({ fullBreakdown: true }));
415
+ // "2 meses y 5 días"
416
+ ```
417
+
418
+ #### Opciones de Humanize
419
+
420
+ | Opción | Tipo | Default | Descripción |
421
+ | --------------- | ------------------ | ------------- | ----------------------------------------------- |
422
+ | `locale` | string | del TimeGuard | Código de locale ('en', 'es', 'fr', etc.) |
423
+ | `fullBreakdown` | boolean | false | Mostrar desglose completo vs. unidad más grande |
424
+ | `numeric` | 'always' \| 'auto' | 'always' | Formato numérico para Intl API |
425
+
426
+ #### Locales Soportados
427
+
428
+ Humanize automáticamente soporta:
429
+
430
+ - 🇬🇧 **en** - English
431
+ - 🇪🇸 **es** - Español
432
+ - 🇫🇷 **fr** - Français (French)
433
+ - 🇩🇪 **de** - Deutsch (German)
434
+ - 🇮🇹 **it** - Italiano (Italian)
435
+ - 🇵🇹 **pt** - Português (Portuguese)
436
+
437
+ Y muchos más a través de `Intl.RelativeTimeFormat`.
438
+
439
+ ### 💥 Explain Mode: Debugging Brutal y Educación
440
+
441
+ **Feature killer para debugging y enseñanza** - Obtén una explicación detallada de cómo se calculó cada duración.
442
+
443
+ Perfecto para:
444
+
445
+ - 🐛 **Debugging** - Entender exactamente cómo se calculó la duración
446
+ - 📚 **Educación** - Enseñar matemáticas de fechas
447
+ - 📋 **Auditoría** - Verificar lógica de negocio basada en fechas
448
+ - ✅ **Validación** - Confirmar que los cálculos son correctos
449
+
450
+ ```typescript
451
+ const start = TimeGuard.from("2024-01-15");
452
+ const end = TimeGuard.from("2024-03-20");
453
+ const duration = start.until(end);
454
+
455
+ // Explicación detallada del cálculo
456
+ const explanation = duration.explain();
457
+
458
+ console.log(explanation);
459
+ // {
460
+ // input: [
461
+ // '2024-01-15T00:00:00',
462
+ // '2024-03-20T00:00:00'
463
+ // ],
464
+ // steps: [
465
+ // 'Parsed dates: 2024-01-15 (day 15 of 365) to 2024-03-20 (day 80 of 365)',
466
+ // '2024 is a leap year (February has 29 days)',
467
+ // 'Years: 0',
468
+ // 'Months: 2',
469
+ // 'Days: 5',
470
+ // 'Total: 2m 5d'
471
+ // ],
472
+ // breakdown: {
473
+ // years: 0,
474
+ // months: 2,
475
+ // weeks: 0,
476
+ // days: 5,
477
+ // hours: 0,
478
+ // minutes: 0,
479
+ // seconds: 0,
480
+ // milliseconds: 0
481
+ // },
482
+ // mode: 'exact',
483
+ // explanation: 'Calculated 2024-01-15 to 2024-03-20. 2024 is a leap year. Breakdown: 0 year(s), 2 month(s), 5 day(s). Mode: exact calculation',
484
+ // locale: 'en',
485
+ // leapYearFlags: [
486
+ // { year: 2024, isLeap: true, daysInFebruary: 29 }
487
+ // ],
488
+ // metadata: {
489
+ // calculationTimeMs: 0.5,
490
+ // precision: 'day'
491
+ // }
492
+ // }
493
+ ```
494
+
495
+ #### Casos de Uso
496
+
497
+ Debugging de lógica de fechas:
498
+
499
+ ```typescript
500
+ // ¿Por qué el cálculo de mora es diferente?
501
+ const invoiceDate = TimeGuard.from("2024-01-05");
502
+ const paymentDate = TimeGuard.from("2024-02-15");
503
+ const lateFee = invoiceDate.until(paymentDate);
504
+
505
+ // Obtén la explicación completa
506
+ const debug = lateFee.explain();
507
+
508
+ console.log("Pasos del cálculo:", debug.steps);
509
+ console.log("Febrero es bisiesto:", debug.leapYearFlags);
510
+ console.log("Desglose:", debug.breakdown);
511
+ // Ahora sabes exactamente qué sucedió!
512
+ ```
513
+
514
+ Educación - Enseñar cálculos de fechas:
515
+
516
+ ```typescript
517
+ // Mostrar a estudiantes cómo funciona
518
+ const start = TimeGuard.from("2024-02-15"); // Enero
519
+ const end = TimeGuard.from("2024-04-15"); // Abril
520
+ const duration = start.until(end);
521
+
522
+ const explanation = duration.explain();
523
+
524
+ // Mostrar todos los pasos
525
+ explanation.steps.forEach((step, i) => {
526
+ console.log(`${i + 1}. ${step}`);
527
+ });
528
+ // 1. Parsed dates: ...
529
+ // 2. 2024 is a leap year...
530
+ // 3. Years: 0
531
+ // 4. Months: 2
532
+ // 5. Days: 0
533
+ // 6. Total: 2m
534
+ ```
535
+
536
+ #### Propiedades de Explanation
537
+
538
+ | Propiedad | Tipo | Descripción |
539
+ | ---------------- | ------------------------ | ----------------------------- |
540
+ | `input` | `string[]` | Fechas de entrada parseadas |
541
+ | `steps` | `string[]` | Pasos del cálculo legibles |
542
+ | `breakdown` | `DurationParts` | Desglose por componentes |
543
+ | `mode` | `'exact' \| 'estimated'` | Tipo de cálculo realizado |
544
+ | `explanation` | `string` | Resumen en texto natural |
545
+ | `locale` | `string` | Idioma de la explicación |
546
+ | `leapYearFlags?` | `Array` | Información de años bisiestos |
547
+ | `metadata?` | `Object` | Rendimiento y precisión |
256
548
 
257
549
  ```typescript
258
550
  const date = TimeGuard.from("2024-03-13 14:35:47.654");
@@ -1042,10 +1334,30 @@ date1.isBetween(date1, date2, undefined, "[]"); // inclusive both ends
1042
1334
  date1.isBetween(date1, date2, undefined, "()"); // exclusive both ends
1043
1335
  date1.isBetween(date1, date2, "month", "[]"); // granular range
1044
1336
 
1045
- // Calculate difference
1337
+ // Calculate difference - Traditional API (backward compatible)
1046
1338
  date1.diff(date2, "days"); // -7
1047
1339
  date1.diff(date2, "millisecond"); // difference in ms
1048
1340
  date1.diff(date2, "months"); // -0
1341
+
1342
+ // Calculate difference - Modern Calendar-Aware API
1343
+ const start = TimeGuard.from("2024-01-15");
1344
+ const end = TimeGuard.from("2024-03-20");
1345
+
1346
+ // Exact mode (default) - precise time difference
1347
+ const exactDiff = start.diff(end, { mode: "exact" });
1348
+ console.log(exactDiff.as("day")); // 65 days
1349
+
1350
+ // Calendar mode - human-readable breakdown
1351
+ const calendarDiff = start.diff(end, { mode: "calendar" });
1352
+ console.log(calendarDiff.format("en")); // "2 months and 5 days"
1353
+ console.log(calendarDiff.format("es")); // "2 meses y 5 días"
1354
+ console.log(calendarDiff.breakdown()); // { years: 0, months: 2, weeks: 0, days: 5, ... }
1355
+
1356
+ // Fluent API for unit conversion
1357
+ const diff = start.diff(end);
1358
+ console.log(diff.as("day")); // 65
1359
+ console.log(diff.as("hour")); // 1560
1360
+ console.log(diff.valueOf()); // milliseconds
1049
1361
  ```
1050
1362
 
1051
1363
  ### 📊 Advanced Calculations
@@ -1187,6 +1499,107 @@ date.format('DD/MM/YYYY "at" HH:mm'); // 13/03/2024 at 14:30
1187
1499
 
1188
1500
  **📖 Complete Format Guide:** See [EXAMPLES.md](EXAMPLES.md) for more patterns and use cases.
1189
1501
 
1502
+ ### ⏱️ Calendar-Aware Difference Calculations
1503
+
1504
+ One of TimeGuard's most powerful features is its **calendar-aware diff mode**, which solves a fundamental problem with date libraries: months and years have variable lengths.
1505
+
1506
+ #### Understanding Two Approaches
1507
+
1508
+ ```typescript
1509
+ // Problem: How much time between Jan 15 and Mar 20?
1510
+ const start = TimeGuard.from("2024-01-15");
1511
+ const end = TimeGuard.from("2024-03-20");
1512
+
1513
+ // ❌ Naive approach: Just count days
1514
+ // Result: "65 days" (technically correct but not user-friendly)
1515
+
1516
+ // ✅ Calendar-aware approach: Break into meaningful units
1517
+ // Result: "2 months and 5 days" (how humans think about time)
1518
+ ```
1519
+
1520
+ #### API Modes
1521
+
1522
+ ```typescript
1523
+ const start = TimeGuard.from("2024-01-15");
1524
+ const end = TimeGuard.from("2024-03-20");
1525
+
1526
+ // ==== EXACT MODE (default) ====
1527
+ // Precise time difference in specified unit
1528
+ const exactDiff = start.diff(end, { mode: "exact" });
1529
+
1530
+ // Returns normalized difference
1531
+ exactDiff.as("day"); // 65 days
1532
+ exactDiff.as("hour"); // 1560 hours
1533
+ exactDiff.as("week"); // 9 weeks
1534
+ exactDiff.valueOf(); // milliseconds
1535
+
1536
+ // ==== CALENDAR MODE ====
1537
+ // Human-readable breakdown with automatic localization
1538
+ const calendarDiff = start.diff(end, { mode: "calendar" });
1539
+
1540
+ // Get structured breakdown
1541
+ calendarDiff.breakdown();
1542
+ // {
1543
+ // years: 0,
1544
+ // months: 2,
1545
+ // weeks: 0,
1546
+ // days: 5,
1547
+ // hours: 0,
1548
+ // minutes: 0,
1549
+ // seconds: 0,
1550
+ // milliseconds: 0
1551
+ // }
1552
+
1553
+ // Format in any locale
1554
+ calendarDiff.format("en"); // "2 months and 5 days"
1555
+ calendarDiff.format("es"); // "2 meses y 5 días"
1556
+ calendarDiff.format("fr"); // "2 mois et 5 jours"
1557
+ calendarDiff.format("de"); // "2 Monate und 5 Tage"
1558
+ ```
1559
+
1560
+ #### Backward Compatibility
1561
+
1562
+ ```typescript
1563
+ // Traditional API still works perfectly
1564
+ const tg1 = TimeGuard.from("2024-01-15");
1565
+ const tg2 = TimeGuard.from("2024-03-20");
1566
+
1567
+ tg1.diff(tg2, "day"); // 65 (returns number)
1568
+ tg1.diff(tg2, "month"); // 2
1569
+ tg1.diff(tg2, "millisecond"); // raw milliseconds
1570
+ ```
1571
+
1572
+ #### Advanced Usage
1573
+
1574
+ ```typescript
1575
+ const start = TimeGuard.from("2024-01-15 10:30:00");
1576
+ const end = TimeGuard.from("2024-03-20 15:45:00");
1577
+
1578
+ // Fluent API for unit conversion
1579
+ const diff = start.diff(end);
1580
+ diff.as("day"); // Get as days
1581
+ diff.as("hour"); // Get as hours
1582
+ diff.as("minute"); // Get as minutes
1583
+ diff.valueOf(); // Implicit number conversion (milliseconds)
1584
+
1585
+ // Locale-specific formatting
1586
+ const spanishDiff = start.diff(end, {
1587
+ mode: "calendar",
1588
+ locale: "es",
1589
+ });
1590
+ console.log(spanishDiff.format()); // Uses Spanish locale
1591
+
1592
+ // Query the mode used
1593
+ diff.getMode(); // "exact"
1594
+ calendarDiff.getMode(); // "calendar"
1595
+
1596
+ // String representation
1597
+ spanishDiff.toString(); // "2 meses y 5 días" (nice for display)
1598
+ exactDiff.toString(); // "432000000" (milliseconds)
1599
+ ```
1600
+
1601
+ **💡 Key Insight:** Calendar mode handles month/year variations automatically using Temporal API's built-in month normalization, ensuring accurate results across different month lengths and leap years.
1602
+
1190
1603
  ---
1191
1604
 
1192
1605
  ## 🧪 Testing
@@ -1,4 +1,4 @@
1
- /*! time-guard v2.0.8 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
1
+ /*! time-guard v2.2.0 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
2
2
  //#region src/calendars/index.ts
3
3
  var e = class {
4
4
  id = "islamic";
@@ -1,4 +1,4 @@
1
- /*! time-guard v2.0.8 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
1
+ /*! time-guard v2.2.0 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
2
2
  //#region src/locales/english.locale.ts
3
3
  var e = {
4
4
  name: "en",
@@ -1,4 +1,4 @@
1
- /*! time-guard v2.0.8 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
1
+ /*! time-guard v2.2.0 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
2
2
  //#region src/plugins/advanced-format/index.ts
3
3
  var e = class {
4
4
  name = "advanced-format";
@@ -1,4 +1,4 @@
1
- /*! time-guard v2.0.8 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
1
+ /*! time-guard v2.2.0 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
2
2
  //#region src/plugins/duration/index.ts
3
3
  var e = class e {
4
4
  years = 0;
@@ -1,4 +1,4 @@
1
- /*! time-guard v2.0.8 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
1
+ /*! time-guard v2.2.0 | (c) 2026 Berea-Soft | MIT License | https://github.com/Berea-Soft/time-guard */
2
2
  //#region src/plugins/relative-time/index.ts
3
3
  var e = [
4
4
  {