Every Built-in Member: DateTime, Duration, Uri, Object and friends
After this lesson you will know that a DateTime is just one integer plus a flag, why adding "one day" can give two different answers on a daylight-saving date, how Uri.parse cuts an address into pieces, what the == / hashCode contract is and exactly what goes wrong inside a HashSet when it is broken. And you will have seen every public member of DateTime, Duration, Uri, UriData, Object, Comparable, Function and Stopwatch, plus identical, identityHashCode and Object.hash / hashAll / hashAllUnordered, each with its cost, its exceptions, whether it changes the object or returns a new one, and a runnable example whose exact output was checked against the real Dart SDK.
Related lessons: D09 classes and objects (how operator == and hashCode are overridden), D22 mutable vs immutable (value vs reference, const canonicalisation, mutable keys), D27 Set and Map (the hash tables behind HashSet and HashMap), and D26 List (sort and Comparable). Source of truth: the Dart SDK 3.11 sources (core/date_time.dart, duration.dart, uri.dart, object.dart, comparable.dart, function.dart, stopwatch.dart, identical.dart, the VM implementations in _internal/vm_shared/lib and internal/internal.dart). Everything below describes the native VM; on the web (dart2js) int is a JavaScript number and some numbers differ. Every printed result was produced by running the code with Dart 3.11 and is asserted in verify/d29.dart; examples that depend on a time zone state which one they ran in.
1. DateTime inside: one integer and one flag
Think of a stopwatch that was started at one fixed moment in history and has never been stopped. Reading it tells you "this many microseconds have passed". That single number is the moment. "9 March 2024, 14:30" is only a translation of the number into words, the way "3.2 km" is a translation of a number of metres. The translation depends on which wall clock you read it from: the world clock that never moves (UTC) or the clock on your own wall (your local time zone). A DateTime keeps the number and a flag that says which clock to read it from.
Words, defined once. A microsecond is one millionth of a second. The epoch is the fixed starting moment: 00:00:00 on 1 January 1970 in UTC (Coordinated Universal Time, the zone-free world clock; it never changes for summer time). A time zone is a rule "wall clocks here read UTC plus some offset" (+5:30 in India, −5:00 or −4:00 in New York). Daylight saving time (DST) means a region moves its clocks by an hour twice a year, so its offset changes during the year.
Inside the object there are three fields: _value (the microseconds, a 64-bit integer), isUtc (the flag) and a lazily created cache of the nine calendar fields. The calendar fields are computed from the integer by division (this is the same arithmetic you would do by hand). Here are the helper functions the animation runs, written in plain Dart (civilFromDays is the standard "days since 1970 to year-month-day" algorithm used by many libraries; the SDK uses a table-driven equivalent that gives identical results, which verify/d29.dart checks for every day from year 1 to year 9999):
Now watch one number turn into a calendar date. The player starts from 1709994605250123 and shows every division; type another number (or a UTC time such as 2000-02-29T23:59:59Z) to try your own.
Input: a whole number of microseconds from −62135596800000000 (year 1) to 253402300799999999 (year 9999), or a UTC moment YYYY-MM-DDTHH:MM:SS.ffffffZ (seconds and fraction optional).
Input size → feasible: every field needs a handful of integer divisions and no loop over years, so reading fields is O(1) and ten million per second is easy; the arithmetic never depends on how far from 1970 the date is.
The same number can be read on two clocks. toLocal() and toUtc() do not change the number: they make a second object with the other flag. That is why == (which compares number and flag) can say "different" for the same moment, while isAtSameMomentAs (number only) says "same".
Input: UTC moment ; offset (offset from -12:00 to +14:00 in steps of 15 minutes). This player models a zone with a fixed offset; section 2 adds daylight saving.
DateTime as text: toString() of a local time has no offset at all, so the same text means different moments on different machines. Store toUtc().toIso8601String() (it ends in Z) or millisecondsSinceEpoch, and convert to local only when showing it to a person._value is an int, isUtc a bool, and __parts a nullable List<int> filled the first time any calendar getter is read. hashCode is (_value ^ (_value >> 30)) & 0x3FFFFFFF and ignores isUtc; == compares _value and isUtc. The allowed range is ±8,640,000,000,000,000 ms (about ±275,760 years); outside it the constructors throw.DateTime is an instant (one integer) plus a display flag. Arithmetic (add, subtract, difference, the comparisons) works on the integer only; the calendar fields and == care about the flag.2. Time zones and daylight saving: two ways to add a day
Imagine a long train timetable where, one night a year, the station clock is pushed forward one hour: after 01:59 it shows 03:00, so there is no 02:30 that night. Another night it is pushed back: 01:30 happens twice. "Add one day" now has two reasonable meanings. "Twenty-four hours from now" counts real elapsed time (a ticket that expires in 24 hours). "Same time tomorrow" counts calendar days on the wall clock (a daily 12:00 reminder). On an ordinary day they agree; on a switch day they differ by an hour.
start.add(Duration(days: 1)) is the first meaning: it adds exactly 86,400 seconds to the integer and then reads the result on the wall clock. DateTime(y, m, d + 1, h, min) is the second: it builds the wall-clock reading first (the day field rolls over by itself at month end) and then asks the time-zone rules which instant that is. The player uses the real 2024 rules of New York (spring forward on 10 March at 02:00, fall back on 3 November at 02:00); every result it shows was compared with the real Dart SDK running in that zone for all 13,176 combinations of date, time of day and number of days in the check file.
Input: YYYY-MM-DD HH:MM ; N (a 2024 date and N whole days, 0 to 30).
On the clock-moves-back day the same two calls disagree the other way: the calendar day is 25 hours long.
Edge case. Asking for a wall-clock time that does not exist (02:30 on the spring-forward day) or that happens twice (01:30 on the fall-back day) has no single correct answer, so the SDK asks the operating system and uses whatever the system rule says. The animation reports what Dart 3.11 on macOS does (the missing 02:30 becomes 03:30; the repeated 01:30 is the first one); another platform may resolve it differently, so avoid scheduling anything between 02:00 and 03:00 local time on those days.
A related trap: difference(...).inDays counts 24-hour blocks, not calendar days. Between midnight on 10 March and midnight on 11 March in New York only 23 hours pass:
| You mean… | Use | Why |
|---|---|---|
| a timeout, token or cache that lasts exactly 24 hours | now.add(Duration(hours: 24)) | elapsed time; unaffected by clock changes |
| "every day at 12:00" in the user’s zone | DateTime(y, m, d + 1, 12) | wall-clock time; the SDK finds the right instant |
| number of calendar days between two dates | compare DateTime.utc(y, m, d) values (UTC midnights are always exactly 24 h apart) | no DST in UTC |
| anything stored or sent over the network | UTC (toUtc()) or epoch milliseconds | unambiguous |
add / subtract / difference are elapsed-time operations (they ignore the calendar). The fields constructor, copyWith and DateTime.parse without a zone are wall-clock operations (they consult the time-zone rules). In UTC the two are identical.3. Every member, grouped by type
Helper definitions. A few cards below use small classes and a function that are not part of the Dart SDK (Plain, Point, Version, Ghost, Greeter, describe). They are defined once here: paste this block above a card’s code (outside main) to run it. The cards that call sleep(...) also need import 'dart:io';, and the ones using HashSet need import 'dart:collection';.
Each card shows: the signature, what it means in plain words, a badge telling you what happens to the object, the cost and why, the exceptions, and a runnable example with its exact output. Some cards cover several members that are one-line variations of each other; the member list under the heading says which. Examples that depend on the machine’s time zone say which zone they ran in (the Dart code of such a card does not change; only the output does).
| Badge | Meaning |
|---|---|
| mutates | changes this object (only Stopwatch in this lesson) |
| returns new | builds a new object (a DateTime, Duration, Uri, string, list or map); the original is untouched |
| read-only | reads or reports; changes nothing, allocates nothing important |
| creates | a constructor or factory: makes a new object |
DateTime, 38 of Duration, 50 of Uri, 16 of UriData, 9 of Object, 3 of Comparable, 4 of Function, 10 of Stopwatch, 2 (the two top-level functions identical and identityHashCode) (constants, constructors, factories, getters, operators, methods and static helpers; Comparator and DateTime.copyWith are counted with their types). Not deprecated: no member of these types is marked @Deprecated in Dart 3.11. Not a class member: DateTime.copyWith is an extension (DateTimeCopyWith, since Dart 2.19) declared next to DateTime in dart:core, Comparator is a type alias, and identical, identityHashCode are top-level functions; all are covered because you meet them as if they were members.Index of every member (click a name to jump to its card)
3.1 DateTime: constants, constructors, parse and format
DateTime has six constructors (the fields constructor, utc, now, timestamp and the two epoch constructors), two static parsers (parse and tryParse) and 21 numeric constants. Every one builds a new object because a DateTime is immutable.
3.2 DateTime: read and compare
Reading is cheap and never changes the object. Remember the rule from section 1: the calendar fields and == depend on the zone flag; the ordering members look only at the number.
3.3 DateTime: arithmetic and conversion
These return a new DateTime. Section 2 animated the difference between add (elapsed time) and the fields constructor (wall-clock time).
3.4 Duration
A Duration is a length of a stretch of time, like a ruler measurement: "90 minutes" says nothing about when. It is stored as one integer: microseconds. That is why it has no months or years (a month is 28 to 31 days; there is no single length) and why every unit getter is just a division.
The three players show how the constructor builds the integer, how the operators work on it, and how the inX getters truncate. A Duration is immutable: every operator returns a new one.
Input: parts like 1d 25h 90m (units d, h, m, s, ms, us; a whole number each, negative allowed, each unit at most once).
Input: a starting duration, then up to six operations separated by ;: + 45m, - 2h, * 1.5 (whole number or decimal), ~/ 4 (a whole number), abs, neg. A division by zero stops with IntegerDivisionByZeroException.
Input: a whole number of microseconds (up to 15 digits, negative allowed).
Input size → feasible: every Duration operation is one integer operation plus one small allocation; summing a million durations takes a few milliseconds. The only limit is the 64-bit range (about ±292,000 years in microseconds) and, on the web, 253 microseconds (about 285 years).
3.5 Uri
A web address is like a postal address written on one line: country: (the scheme: how to deliver, https), then // the building (the authority: user, host, port), then the path to the room (/a/b), then a ?query (a form to fill in: x=1&y=2), then a #fragment (a page marker you read yourself). A Uri object holds those seven pieces separately, already cleaned up, so you never cut the string by hand.
The first player shows one left-to-right pass of Uri.parse over a full address, colouring each piece. The second shows the two shapes that are not a full web address: a relative reference (no scheme, no host) and a mailto: address (a scheme without //).
Input: any URI text up to 160 characters (printable ASCII or UTF-8). An invalid one shows the FormatException that Uri.parse would throw. data: URIs (see the UriData section), a file: URI written with one slash, and a path that is just ., ends in /. or starts with ./ are outside this animation: the SDK handles them through special fast paths.
The next two players show resolve (how a link inside a page becomes a full address) and the three percent-encoders. Both were compared with the real SDK on thousands of random inputs in the check file.
Input: base ; reference with a space on both sides of the semicolon. The base needs a scheme.
Input: up to 12 characters of any text.
Input size → feasible: parsing, resolving and encoding are single passes over the text, O(L) for L characters: a URL of 2,000 characters takes microseconds and a million of them a second or two. The cost to avoid is parsing the same string again and again: parse once and keep the Uri.
Creating a Uri
Reading the pieces
Changing and combining (these return a new Uri)
Static helpers: encoding, decoding, IP addresses
3.6 UriData
A data: URI carries its content inside the address (data:text/plain;charset=utf-8,hello%20world), so a small image or text needs no server. UriData reads and writes that format: the part before the comma is a header (MIME type, parameters, optional base64 marker) and the part after is the payload.
A data: URI is like a parcel with the contents written on the label: instead of "go to this address and fetch the file", the address is the file. The label has a short header ("this is HTML, written in UTF-8") and then the content, which must be written using only safe characters: either percent-escapes (a byte that is not safe becomes %XX) or base64 (every 3 bytes become 4 letters).
Three players: building a data URI from text, building one from raw bytes (the base64 step by step), and reading one back. Each was compared with the real UriData on hundreds of inputs in the check file.
Input: text ; mime with a space on both sides of the semicolon (up to 14 characters of text; the MIME type, like text/html, is optional).
Input: 1 to 12 byte values (0 to 255) separated by commas.
Input: a data: URI of printable ASCII characters (use %C3%A9 style escapes for anything else), with a comma, at most 120 characters. A malformed one shows the FormatException that the SDK would throw. Charsets understood by contentAsString here: utf-8, us-ascii, latin1 / iso-8859-1; any other name gives the UnsupportedError the SDK throws.
Input size → feasible: encoding and decoding are single passes (O(n) for n bytes); base64 makes the text 4/3 as long, percent-encoding between 1× and 3×. A data URI is for small things (icons, tiny files): a 1 MB image becomes a 1.4 MB string in memory that every consumer must copy.
3.7 Object, identical, identityHashCode and the hash functions
Every value in Dart is an Object, so these members exist on everything. The important pair is == and hashCode; section 6 animates the contract between them. identical and identityHashCode are the escape hatch that ignores any override.
The last member of Object is the strangest: noSuchMethod is called by the runtime when a dynamic call finds no such member. The player shows the Invocation object that carries the call.
3.8 Comparable and Function
A class that implements Comparable<T> promises a natural order through compareTo. list.sort() with no argument calls it. For a list of at most 32 elements the SDK runs an insertion sort, so you can watch every call:
Input: 2 to 8 versions as major.minor, separated by commas.
A comparator is a function with the same sign rules as compareTo that you pass to sort to override the natural order. Swapping the two arguments gives the reverse order:
Every function is an object too: Function.apply calls one with an argument list built at run time, matching positional arguments by position and named arguments by Symbol.
Input: positional ; named, for example 1, 2 ; d=9, c=5 (whole numbers; the named part is optional). Too many, too few or unknown arguments show the NoSuchMethodError.
Functions are compared too (Function.==, Function.hashCode). The rule surprises people: two tear-offs of the same method of the same object are equal, two separately written closures never are, which is why removeListener(() => ...) with a fresh closure silently does nothing.
3.9 Stopwatch
A Stopwatch measures elapsed time using a monotonic clock (one that only moves forward, unlike the wall clock that the user or the network can set backwards). It is the one mutable type in this lesson: it holds two integers, _start and _stop, and start, stop, reset rewrite them. The player runs the SDK’s own logic (copied from stopwatch.dart, with the clock turned into a field you can set) on a virtual clock so every number is predictable:
Input: up to 10 steps separated by ;: start, stop, reset, wait N (N milliseconds, 1 to 9999).
4. Mutable vs immutable: which of these types can change
Almost everything in this lesson is immutable: once built, a DateTime, Duration or Uri never changes, and every "change" returns a new object (see D22 for why that is safe to share and to use as a map key). The exception is Stopwatch.
| Type | Can it change after creation? | "Change" it by… | const? | Notes |
|---|---|---|---|---|
DateTime | no (final fields) | add, subtract, copyWith, toUtc, toLocal | no const constructor | safe as a Map key; equal values are separate objects |
Duration | no | operators + - * ~/, abs, unary - | yes: const Duration(seconds: 5) is canonical | constants in a const list or default parameter cost nothing |
Uri | no | replace, removeFragment, resolve, normalizePath | no | pathSegments, queryParameters, queryParametersAll are unmodifiable (writes throw UnsupportedError) |
UriData | no | build a new one | no | parameters returns a fresh modifiable map each call; changing it does not change the UriData |
Stopwatch | yes | start, stop, reset | no | do not share one between unrelated measurements |
Object | the bare Object() has no fields; your subclasses decide | — | const Object() is canonical | keep fields used by ==/hashCode final |
| functions | a function value never changes | — | tear-offs of top-level/static functions are canonical constants | closures capture variables, and those can change |
Proof by running (each line was executed):
HashSet or used as a HashMap key changes a field that == and hashCode use, the collection still files it under the old hash, so it can no longer be found (section 6 animates this). Keep such fields final, or never mutate an object while it is a key.5. Choosing: how to store a moment, a length of time, an address
| To store… | Best choice | Why | Avoid |
|---|---|---|---|
| a moment in a database or an API | epoch milliseconds (int) or an ISO 8601 UTC text (2024-03-09T14:30:00.000Z) | unambiguous, sorts correctly, 8 bytes or 24 characters | local time text (no offset); DateTime.toString() of a local time |
| a moment you compute with in memory | DateTime in UTC | calendar fields and arithmetic ready; no DST surprises | mixing local and UTC objects in == |
| a recurring local-time event | local calendar fields + the zone name (separately), rebuilt with DateTime(y, m, d, h, min) | "every day at 12:00" must follow the zone rules | adding Duration(days: 1) repeatedly |
| a length of time | Duration | units are carried by the type; arithmetic and comparison built in | a bare int of "seconds" (which unit?) |
| a span measured in months or years | a calendar computation on fields (copyWith(month: ...)) | Duration has no months | 30-day months |
| timing code | Stopwatch | monotonic clock | DateTime.now() differences (the wall clock can jump) |
| an address you build or analyse | Uri | escapes for you, compares normalised, gives pieces | string concatenation and regexes |
| a file path | Uri.file / toFilePath only when you need a URI; otherwise a String path | URIs add escaping | building file: text by hand |
| Question | identical(a, b) | a == b | a.compareTo(b) == 0 |
|---|---|---|---|
| means | the very same object | "equal" as the class defines it | "tie in the natural order" |
| cost | O(1), pointer comparison | O(fields compared) | O(fields compared) |
| can be overridden | never | yes | yes (Comparable) |
| use for | identity sets (Set.identity()), caches keyed by object, const checks | sets, maps, contains, remove | sorting, SplayTreeSet |
| Operation | Cost | Why |
|---|---|---|
DateTime getters, comparisons, add, difference | O(1) | integer arithmetic (the first calendar getter also fills a 9-int cache) |
DateTime.parse, toString | O(L) | one pass over the text |
Duration operators and getters | O(1) | one integer operation |
Uri.parse, resolve, encoders | O(L) | single pass over the text |
Uri.queryParameters (first read) | O(query length) | split and decode once, then cached |
Uri ==, hashCode | O(L) | compares or hashes the normalised text |
Object.hash of k values | O(k) | one combine step per value |
HashSet.contains / add (good hash) | O(1) expected | hash picks the bucket; short chains |
HashSet.contains / add (all hashes equal) | O(n) | one long chain: every entry is compared with == |
list.sort() of n comparables | O(n log n) expected | insertion sort up to 32 elements, then dual-pivot quicksort |
6. The == / hashCode contract and what breaks in a HashSet
A big library files volumes on shelves by a shelf number computed from the title (hashCode). To check whether a volume is in the library, the librarian computes the shelf number of the volume you hold, walks to only that shelf and compares your volume with each volume there (==). That is fast, but only if two copies of the same volume always get the same shelf number. If the shelf number secretly depends on something else (the date it was printed, its position in the queue), the librarian looks on the wrong shelf and says "not here" about a volume that is in the building.
The contract (from the documentation of Object):
- Equal objects have equal hash codes: if
a == bthena.hashCode == b.hashCode. (The reverse is not required: different objects may share a hash; that is a collision and only costs time.) ==is reflexive, symmetric, transitive, andx == nullis false.- Both must give the same answer every time as long as the fields they read do not change: consistent. So a hash collection must never contain an object whose
==/hashCodefields have been mutated.
Three small classes, one per way to get it wrong or right. x * 31 + y is a simple multiplicative hash (the real Object.hash(x, y) mixes better and is the right default; a small hash makes the bucket numbers easy to follow):
Now watch a real HashSet (the SDK’s own chained-bucket table: 8 buckets at first, bucket = hash & (buckets - 1), a new entry goes at the front of its chain, the table doubles when more than three quarters full). The picture is checked against the real HashSet (results of every operation, final length and iteration order) on 152 random scripts. First the correct contract:
Now the same script with a hash that uses a field == ignores (serial, a counter that gives every object a different number). Equal points get different hashes, so they are filed in different buckets and never compared:
The third classic bug: the object is fine when added, then mutated while it is in the set. The set does not know; the entry stays in the bucket of its old hash.
Input: mode ok, mode bad or mode hashOnly, then up to 14 operations separated by ;: add x,y, has x,y, remove x,y, mut #n x,y (x and y whole numbers from -999 to 999; #n is the serial number of the n-th object created, counting every add, has and remove; a mutation needs a serial that already exists).
The mirror-image mistake: override hashCode but not ==. Equal hashes send two points to the same bucket, but == is still identity, so they are different. This player also adds enough entries to trigger the table doubling:
== but unequal hash → add stores duplicates and contains says false for a value that is "in" the set. Equal hash but identity == → duplicates by value, correct but useless. Mutated key → lost entry that still counts in length and still appears when you iterate. A constant hash such as hashCode => 1 is legal (the contract holds) but puts everything in one bucket: contains becomes O(n) and filling the set O(n²).final. (2) Override == with other is T && field comparisons. (3) Override hashCode with Object.hash(field1, field2, ...) over exactly the same fields. (4) Override toString. For a field that is a collection use Object.hashAll (order matters) or hashAllUnordered (order does not).Inside Object.hash: combine, combine, finish
Object.hash(a, b, c) is not magic: it starts from a per-run seed, mixes in each value’s hashCode with combine, then scrambles once with finish. Because each step uses the previous result, the order of the values changes the answer. The player below is a line-by-line port of SystemHash in the SDK (internal/internal.dart) and was compared with the real Object.hashAll and Object.hashAllUnordered on 86 lists of numbers (using the real seed of that run). In this Dart version an int’s own hashCode is itself a scramble (1.hashCode is 11,601), shown as the first row of each step.
The same three functions as Dart code (the text of SystemHash in dart:_internal; smear is used only by hashAllUnordered):
Input: seed ; values, for example 0 ; 1, 2, 3 (seed 0 to 999999999, 1 to 5 values 0 to 999999999). The real seed is a different number on every run, which is exactly why you must never store an Object.hash result.
When equality means "same elements in any order", use hashAllUnordered: it scrambles each element alone and adds the results, and addition ignores order:
Input: the same format. Try 0 ; 5, 5 to see that the count matters, and a palindrome such as 0 ; 1, 2, 1 to see that hashAll of a list and its reverse can coincide.
combine(h, v) = ((h + v) & 0x1fffffff, then + ((h & 0x7ffff) << 10), then ^ (h >> 6)), finish adds (h & 0x3ffffff) << 3, xors h >> 11, adds (h & 0x3fff) << 15, always keeping 29 bits, so every result is below 229. Object.hash for 2 to 20 arguments runs the same chain with the seed identityHashCode(Object). hashAllUnordered uses seed 0, so its result is the same in every run of this SDK version (still an implementation detail, not a promise).==, hashCode, identical, identityHashCode, Object.hash, hashAll and hashAllUnordered as individual cards; this section is the story that connects them. Related: D27 shows the same hash table inside Set and Map, and D22 shows why immutable value classes avoid the mutated-key trap.Quiz
Interview questions
Cheat sheet: every member
Generated from the same member data as the cards above: every member with its cost and whether it mutates or returns something new.