gmana_extensions 0.0.1 copy "gmana_extensions: ^0.0.1" to clipboard
gmana_extensions: ^0.0.1 copied to clipboard

Dart extension methods for Duration, String, num, Stream, Iterable, and validation utilities.

gmana_extensions #

Pure Dart extension methods for core Dart types.

import 'package:gmana_extensions/gmana_extensions.dart';

Table of contents #


Duration #

Construction from numbers (NumDurationExtension on num) #

5.seconds           // Duration(seconds: 5)
30.minutes          // Duration(minutes: 30)
2.hours             // Duration(hours: 2)
1.days              // Duration(days: 1)
3.weeks             // Duration(days: 21)
500.milliseconds    // Duration(milliseconds: 500)
500.ms              // same, shorter alias
100.microseconds

// Arithmetic still works
final eta = 1.hours + 30.minutes + 45.seconds;

Arithmetic & clamping (HumanizedDuration) #

final d = 90.minutes;

d * 2                       // Duration(hours: 3)
d / 3                       // Duration(minutes: 30)

d.clamp(1.hours, 2.hours)   // Duration(hours: 1, minutes: 30)
d.coerceAtLeast(2.hours)    // Duration(hours: 2)
d.coerceAtMost(1.hours)     // Duration(hours: 1)

d.abs                       // always non-negative
d.isNegative                // false
d.isPositive                // true
d.isZero                    // false

d.isLongerThan(1.hours)     // true
d.isShorterThan(2.hours)    // true
d.isWithin(15.minutes, 1.hours + 10.minutes) // within 15m of 1h10m?

Rounding #

final d = Duration(minutes: 7, seconds: 40);

d.roundToSeconds()    // Duration(minutes: 7, seconds: 40) — already whole seconds
d.roundToMinutes()    // Duration(minutes: 8)
d.ceilToMinutes()     // Duration(minutes: 8)
d.floorToMinutes()    // Duration(minutes: 7)

Parts & fractions #

final d = Duration(hours: 1, minutes: 23, seconds: 45, milliseconds: 600);

d.hoursPart         // 1
d.minutesPart       // 23
d.secondsPart       // 45
d.millisecondsPart  // 600

d.inHoursDouble     // 1.395...
d.inMinutesDouble   // 83.76
d.inSecondsDouble   // 5025.6
d.inWeeksDouble     // 0.00993...
d.totalHours        // same as inHoursDouble
d.totalDays
d.totalMinutes
d.toSeconds()       // fractional seconds including ms

Progress & remaining #

final elapsed = 3.minutes;
final total = 10.minutes;

elapsed.progressOf(total)              // 0.3
elapsed.progressOf(total, clampResult: false) // can exceed 1.0
elapsed.remainingIn(total)             // Duration(minutes: 7)

Video / animation frames #

const fps = 24.0;
final d = Duration(seconds: 5);

d.toFrames(fps)                 // 120
HumanizedDuration.fromFrames(120, fps) // Duration(seconds: 5)

Async helpers #

await 2.seconds.delay;                       // Future.delayed
final result = await 500.ms.delayed(() => 42); // runs callback after delay

Formatting #

final d = Duration(hours: 1, minutes: 2, seconds: 34);

d.toHuman()            // '1h 2m 34s'  — compact parts
d.toVerboseString()    // '1h 2m 34s'
d.toVerboseString(includeSeconds: false) // '1h 2m'
d.toWordString()       // '1 hour 2 minutes 34 seconds'
d.toHHMMSS()           // '01:02:34' or 'MM:SS' for durations under 1h
d.toHumanizedString()  // '1:02:34'
d.toPaddedString()     // '01:02:34'
d.toRelativeString()   // 'in 1h 2m' / '1h 2m ago'

// Natural language (DurationNaturalLanguageX)
d.toNaturalString()    // '1 hour and 2 minutes'
d.toCompactString()    // '1h 2m'
d.toDetailedString()   // '1h 2m 34s 0ms'

num / int / double / bool #

Nullable coercion #

// int?
int? n;
n.orZero             // 0
n.isNullOrZero       // true
n.orDefault(42)      // 42

// double?
double? d;
d.orZero             // 0.0
d.orDefault(3.14)    // 3.14

// num?
num? x;
x.orZero
x.orDefault(10)

// bool?
bool? b;
b.orFalse            // false
b.orTrue             // true
b.isNullOrFalse      // true

NumX — math on any num #

// Rounding to multiples
17.roundToMultiple(5)    // 15
17.ceilToMultiple(5)     // 20
17.floorToMultiple(5)    // 15
3.14159.roundTo(2)       // 3.14

// Range checks
7.isBetween(1, 10)       // true

// Normalization / lerp
50.normalized(0, 100)           // 0.5  — maps [0,100] → [0,1]
50.normalized(0, 100, 0, 255)   // 127.5 — maps [0,100] → [0,255]
50.normalizedClamped(0, 100)    // 0.5  — same but result clamped to [toMin,toMax]
50.safeNormalized(0, 0, fallback: 0.5) // 0.5 — source range is zero, no throw

0.3.lerp(10, 20)    // 13.0  — t=0.3 between 10 and 20

// Temperature conversion
100.0.celsiusToFahrenheit   // 212.0
212.0.fahrenheitToCelsius   // 100.0
0.0.celsiusToKelvin         // 273.15
300.0.kelvinToCelsius       // 26.85

// Predicates
3.0.isWholeNumber    // true
3.5.isWholeNumber    // false

IntX — int-specific helpers #

42.isEven           // true
7.isOdd             // true
42.digitCount       // 2
42.digits           // [4, 2]
7.isBetween(1, 10)  // true

// Repeat an action
3.times(() => print('hello'));  // prints 3 times

// Inclusive range as lazy iterable
1.to(5)              // [1, 2, 3, 4, 5]
0.to(10, step: 2)    // [0, 2, 4, 6, 8, 10]

String #

Null / blank safety (StringNullableX on String?) #

String? s;
s.orEmpty            // ''
s.isNullOrEmpty      // true
s.isNullOrBlank      // true
s.orNull             // null (coerces blank '' to null)
s.mapNotBlank((v) => v.toUpperCase())  // null if blank, else transformed

Predicates (StringX) #

'hello'.isBlank          // false
'  '.isBlank             // true
'hello'.isNotBlank       // true
'ABC'.isAlpha            // true
'A1B2'.isAlphanumeric    // true
'42'.isNumeric           // true
'hello'.blankToNull      // 'hello'
'  '.blankToNull         // null

Case conversion #

'hello world'.toTitleCase       // 'Hello World'
'hello world'.toSentenceCase    // 'Hello world'
'hello world'.toCamelCase       // 'helloWorld'
'hello world'.toSnakeCase       // 'hello_world'
'hello world'.toKebabCase       // 'hello-world'
'hello world'.toScreamingSnakeCase // 'HELLO_WORLD'
'helloWorld'.toSnakeCase        // 'hello_world'
'helloWorld'.toKebabCase        // 'hello-world'

Slugs & URL helpers #

'Hello World! 2024'.toSlug   // 'hello-world-2024'
'hello'.toUriOrNull          // null (no scheme)
'https://example.com'.toUriOrNull  // Uri(...)
'https://example.com'.isUrl  // true

Parse helpers #

'42'.toIntOrNull     // 42
'abc'.toIntOrNull    // null
'abc'.toIntOrZero    // 0
'3.14'.toDoubleOrNull // 3.14

'true'.toBool        // true
'yes'.toBool         // false  (only 'true' returns true)

'{"a":1}'.jsonDecodeOrNull   // {'a': 1}
'bad json'.jsonDecodeOrNull  // null

'01:30'.toDurationOrNull     // Duration(minutes: 1, seconds: 30)
'1:02:03'.toDuration()       // Duration(hours: 1, minutes: 2, seconds: 3)

Manipulation #

'hello'.repeat(3)                     // 'hellohellohello'
'Hello World'.truncate(8)             // 'Hello...'
'Hello World'.truncate(8, ellipsis: '…') // 'Hello W…'
'Hello World'.truncateWords(8)        // 'Hello...' (breaks at word boundary)
'hello'.wrap('(', ')')                // '(hello)'
'hello'.wrap('<b>')                   // '<b>hello<b>'
'--hello--'.trimHyphens()             // 'hello'
'hello world'.removeWhitespace        // 'helloworld'
'héllo'.reversed                      // 'ollèh' (Unicode-safe)
'hello world'.countOccurrences('l')   // 3
'hello'.hasLengthBetween(3, 10)       // true
'Hello World'.readingTimeMinutes      // 1  (225 wpm estimate)

Validation (StringValidation) #

'user@example.com'.isValidEmail       // true
'+12025551234'.isValidE164Phone       // true
'555-1234'.isValidPhone               // true  (7–15 digits after stripping)
'https://example.com'.isValidUrl      // true
'#FF5500'.isValidHexColor             // true
'192.168.0.1'.isValidIpv4            // true
'2024-01-31'.isValidIsoDate          // true
'550e8400-e29b-41d4-a716-446655440000'.isValidUuid // true
'4111111111111111'.isValidCreditCard // true  (Luhn check)
'John O\'Brien'.isValidName          // true
'Abc123!xyz'.isValidPassword         // true

// Password strength — returns set of unmet requirements
'weak'.passwordStrength   // {PasswordStrength.minLength, PasswordStrength.uppercase, ...}

// Length guard (protects against huge inputs)
'hello'.isWithinLength(min: 3, max: 10)  // true

Date strings (StringDateExtension) #

'2024-01-15'.isDate        // true
'2024-01-15'.isToday       // depends on today
'2020-01-01'.isPast        // true
'2099-12-31'.isFuture      // true
'2024-01-13'.isWeekend     // true  (Saturday)
'2024-01-15'.isWeekday     // true  (Monday)
'2024-01-01'.isLeapYear    // true

'2024-06-01'.isAfter('2024-01-01')    // true
'2024-01-01'.isBefore('2025-01-01')   // true
'2024-06-15'.isBetween('2024-01-01', '2024-12-31') // true

Iterable #

Numeric statistics (IterableNumX on Iterable<num>) #

final nums = [3, 1, 4, 1, 5, 9, 2, 6];

nums.sum()          // 31
nums.product()      // 6480
nums.average        // 3.875
nums.median         // 3.5
nums.minOrNull      // 1
nums.maxOrNull      // 9
nums.range          // 8  (max - min)
nums.variance       // population variance
nums.stdDev         // population standard deviation

nums.top(3)         // [9, 6, 5]  — descending
nums.bottom(3)      // [1, 1, 2]  — ascending

nums.clampAll(2, 7)            // [3, 2, 4, 2, 5, 7, 2, 6]
nums.normalize()               // [0.25, 0.0, 0.375, 0.0, 0.5, 1.0, 0.125, 0.625]
nums.runningSum().toList()     // [3, 4, 8, 9, 14, 23, 25, 31]
nums.runningProduct().toList() // [3, 3, 12, 12, 60, 540, 1080, 6480]

nums.allPositive    // true
nums.allNonNegative // true
nums.allNegative    // false

// With empty-list safety
<int>[].sum()           // 0 (identity default)
<int>[].average         // null
<int>[].averageOrThrow  // throws StateError
<int>[].minOrNull       // null
<int>[].minOrThrow      // throws StateError

General iterable (IterableX) #

final words = ['apple', 'ant', 'banana', 'bear', 'cherry'];

// Group by first letter
words.groupBy((w) => w[0]);
// {'a': ['apple', 'ant'], 'b': ['banana', 'bear'], 'c': ['cherry']}

// Chunk into pages
[1, 2, 3, 4, 5].chunked(2).toList();
// [[1, 2], [3, 4], [5]]

// Distinct by derived key (preserves first-seen order)
['foo', 'bar', 'baz'].distinctBy((s) => s[0]).toList();
// ['foo', 'bar']

// flatMap
['hello', 'world'].flatMap((s) => s.split('').take(2)).toList();
// ['h', 'e', 'w', 'o']

['hello', null, 'world'].flatMapNotNull((s) => s?.split('')).toList();
// ['h','e','l','l','o','w','o','r','l','d']

Flatten (IterableOfIterablesX) #

[[1, 2], [3, 4], [5]].flatten().toList()    // [1, 2, 3, 4, 5]
[[1, 2], [3, 4], [5]].flattenToList()       // [1, 2, 3, 4, 5]

Nullable filtering (IterableNullableX) #

final values = ['a', null, 'b', null, 'c'];

values.whereNotNull.toList()   // ['a', 'b', 'c']
values.compact().toList()      // ['a', 'b', 'c']

values.compactMap((s) => s?.toUpperCase()).toList(); // ['A', 'B', 'C']

Stream #

Operators (StreamX) #

// Debounce — emits only after a quiet period (e.g. search input)
searchController.stream
    .debounce(const Duration(milliseconds: 300))
    .listen(runSearch);

// Throttle — emits the first event, suppresses the rest within the window
buttonTaps.throttle(const Duration(seconds: 1)).listen(submit);

// Distinct — skip repeated values
prefs.onChange
    .distinctUntilChanged()
    .listen(applySettings);

// Custom equality
stream
    .distinctUntilChanged((a, b) => a.id == b.id)
    .listen(update);

// Scan — accumulate state (like redux reduce)
clickStream
    .scan(0, (count, _) => count + 1)
    .listen((count) => print('Clicked $count times'));

// Skip until a condition is met, then pass all subsequent events
stream.skipUntil((v) => v > 10).listen(print);

// Take while (inclusive — includes the first failing value, then closes)
stream.takeWhileInclusive((v) => v < 100).listen(print);

// Indexed — enumerate events
stream.indexed.listen((pair) => print('${pair.$1}: ${pair.$2}'));

// Pairwise — compare consecutive events
prices.pairwise.listen((pair) {
  final delta = pair.$2 - pair.$1;
  print('Change: $delta');
});

// Error recovery
stream.onErrorReturn(0).listen(print);
stream.onErrorReturnWith((e) => -1).listen(print);

// Null filtering
Stream<int?>.value(null).whereNotNull<int>().listen(print); // skips null

// Last value as Future (null if stream closes empty)
final last = await stream.lastOrNull();

List stream operators (StreamListX) #

final itemsStream = Stream<List<int>>.value([3, 1, 4, 1, 5]);

itemsStream.lengths.listen(print);              // 5
itemsStream.whereNotEmpty.listen(print);        // passes [3,1,4,1,5], skips []
itemsStream.filter((n) => n > 2).listen(print); // [3, 4, 5]
itemsStream.mapItems((n) => n * 2).listen(print); // [6, 2, 8, 2, 10]
itemsStream.flatMapItems((n) => [n, -n]).listen(print); // [3,-3,1,-1,4,-4,1,-1,5,-5]
itemsStream.sortedBy((a, b) => a.compareTo(b)).listen(print); // [1,1,3,4,5]
itemsStream.flatten().listen(print);            // 3, 1, 4, 1, 5  (individual ints)
0
likes
160
points
69
downloads

Documentation

API reference

Publisher

verified publishergmana.co

Weekly Downloads

Dart extension methods for Duration, String, num, Stream, Iterable, and validation utilities.

Repository (GitHub)
View/report issues

Topics

#dart #extensions #utilities

License

MIT (license)

Dependencies

gmana_predicates, gmana_validation

More

Packages that depend on gmana_extensions