Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

/// Web `DatePicker dateFormat="MMM d, yyyy"` for the Life date of birth. The
/// stored value stays `yyyy-MM-dd`; this only changes what people see and type.
public enum LifeDateFormat {
private static let months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]

/// `2026-06-10` becomes `Jun 10, 2026`; anything else is returned unchanged.
public static func display(_ stored: String) -> String {
guard let date = LifeMath.parseLifeDate(stored) else { return stored }
let calendar = EffectiveTimeZone.calendar
let year = calendar.component(.year, from: date)
let month = calendar.component(.month, from: date)
let day = calendar.component(.day, from: date)
return "\(months[month - 1]) \(day), \(year)"
}

/// Accepts `yyyy-MM-dd` or `MMM d, yyyy` (comma optional) and returns `yyyy-MM-dd`.
public static func storedValue(from typed: String) -> String? {
let trimmed = typed.trimmingCharacters(in: .whitespacesAndNewlines)
if LifeMath.parseLifeDate(trimmed) != nil { return trimmed }
let parts = trimmed.replacingOccurrences(of: ",", with: " ")
.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
guard parts.count == 3,
let month = months.firstIndex(where: { $0.caseInsensitiveCompare(String(parts[0].prefix(3))) == .orderedSame }),
parts[0].count >= 3,
let day = Int(parts[1]), let year = Int(parts[2])
else { return nil }
let value = String(format: "%04d-%02d-%02d", year, month + 1, day)
return LifeMath.parseLifeDate(value) == nil ? nil : value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation

/// Mirrors `LIFE_QUOTES` in `apps/calendar-web/src/views/Life/life-quotes.ts`.
public enum LifeQuotes {
public static let all: [String] = [
"“Teach us to number our days, that we may gain a heart of wisdom.” (Psalm 90:12)",
"“It is not that we have a short time to live, but that we waste a lot of it.” (Seneca)",
"“How we spend our days is, of course, how we spend our lives.” (Annie Dillard)",
"“How many more times will you watch the full moon rise? Perhaps twenty. And yet it all seems limitless.” (Paul Bowles)",
"“Tell me, what is it you plan to do with your one wild and precious life?” (Mary Oliver)",
"“You could leave life right now. Let that determine what you do and say and think.” (Marcus Aurelius)",
"“Dost thou love life? Then do not squander time, for that is the stuff life is made of.” (Benjamin Franklin)",
"“As if you could kill time without injuring eternity.” (Henry David Thoreau)",
"“Time is the coin of your life. It is the only coin you have, and only you can determine how it will be spent.” (Carl Sandburg)",
"“Life is long, if you know how to use it.” (Seneca)",
"“Your time is limited, so don’t waste it living someone else’s life.” (Steve Jobs)",
"“The average human lifespan is absurdly, terrifyingly, insultingly short.” (Oliver Burkeman)",
"“Forever is composed of nows.” (Emily Dickinson)",
"“Time is the substance I am made of. Time is a river which sweeps me along, but I am the river.” (Jorge Luis Borges)",
"“Begin at once to live, and count each separate day as a separate life.” (Seneca)",
"“While we are postponing, life speeds by.” (Seneca)",
"“Nothing is ours except time.” (Seneca)",
"“Do not act as if you were going to live ten thousand years. Death hangs over you. While you live, while it is in your power, be good.” (Marcus Aurelius)",
"“Think of yourself as dead. You have lived your life. Now take what’s left and live it properly.” (Marcus Aurelius)",
"“It is not death that a man should fear, but he should fear never beginning to live.” (Marcus Aurelius)",
"“Seize the day, put very little trust in tomorrow.” (Horace)",
"“The Moving Finger writes; and, having writ, moves on.” (Omar Khayyam)",
"“To every thing there is a season, and a time to every purpose under the heaven.” (Ecclesiastes 3:1)",
"“Life can only be understood backwards; but it must be lived forwards.” (Soren Kierkegaard)",
"“The unexamined life is not worth living.” (Socrates)",
"“We are always getting ready to live but never living.” (Ralph Waldo Emerson)",
"“Lost time is never found again.” (Benjamin Franklin)",
"“Time is what we want most, but what we use worst.” (William Penn)",
"“The days are long, but the years are short.” (Gretchen Rubin)",
"“Spend the afternoon. You can’t take it with you.” (Annie Dillard)",
"“The trouble is, you think you have time.” (Jack Kornfield)",
"“It passes on just like this, not ceasing day or night.” (Confucius, watching a river)",
"“The months and days are the travelers of eternity. The years that come and go are also voyagers.” (Matsuo Basho)",
"“If not now, when?” (Hillel the Elder)",
"“We are such stuff as dreams are made on, and our little life is rounded with a sleep.” (William Shakespeare)",
"“As a well-spent day brings happy sleep, so a life well spent brings happy death.” (Leonardo da Vinci)",
"“The value of life lies not in the length of days, but in the use we make of them.” (Michel de Montaigne)",
"“Time and tide wait for no man.” (English proverb)",
"“The best time to plant a tree was twenty years ago. The second best time is now.” (Proverb)",
"“A man who dares to waste one hour of time has not discovered the value of life.” (Charles Darwin)",
"“We are like butterflies who flutter for a day and think it is forever.” (Carl Sagan)",
"“Nobody sees a flower really; it is so small. We haven’t time, and to see takes time, like to have a friend takes time.” (Georgia O’Keeffe)",
"“How wonderful it is that nobody need wait a single moment before starting to improve the world.” (Anne Frank)",
"“The purpose of life is to live it, to taste experience to the utmost, to reach out eagerly and without fear for newer and richer experience.” (Eleanor Roosevelt)",
"“I took a deep breath and listened to the old brag of my heart. I am, I am, I am.” (Sylvia Plath)",
"“The time is always right to do what is right.” (Martin Luther King Jr.)",
"“Don’t count the days; make the days count.” (Muhammad Ali)",
"“I urge you to please notice when you are happy, and exclaim or murmur or think at some point, 'If this isn’t nice, I don’t know what is.'” (Kurt Vonnegut)",
"“It is good to have an end to journey toward; but it is the journey that matters, in the end.” (Ursula K. Le Guin)",
"“Life moves pretty fast. If you don’t stop and look around once in a while, you could miss it.” (Ferris Bueller’s Day Off)",
]

public static func random<G: RandomNumberGenerator>(using generator: inout G) -> String {
all.randomElement(using: &generator) ?? all[0]
}

public static func random() -> String {
var generator = SystemRandomNumberGenerator()
return random(using: &generator)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import CompassKit
import XCTest

final class LifeQuotesAndDateFormatTests: XCTestCase {
private var savedZone = "UTC"

override func setUp() {
savedZone = EffectiveTimeZone.identifier
EffectiveTimeZone.identifier = "America/Denver"
}

override func tearDown() {
EffectiveTimeZone.identifier = savedZone
}

func testQuoteListMatchesWebAndEveryQuoteIsAttributed() {
XCTAssertEqual(LifeQuotes.all.count, 50)
XCTAssertTrue(LifeQuotes.all.contains("“Lost time is never found again.” (Benjamin Franklin)"))
XCTAssertTrue(LifeQuotes.all.allSatisfy { $0.hasPrefix("“") && $0.hasSuffix(")") })
XCTAssertTrue(LifeQuotes.all.contains(LifeQuotes.random()))
}

func testDisplayUsesWebsMonthDayYear() {
XCTAssertEqual(LifeDateFormat.display("2026-06-10"), "Jun 10, 2026")
XCTAssertEqual(LifeDateFormat.display("1999-12-01"), "Dec 1, 1999")
XCTAssertEqual(LifeDateFormat.display(""), "")
}

func testTypedDatesAcceptBothFormats() {
XCTAssertEqual(LifeDateFormat.storedValue(from: "Jun 10, 2026"), "2026-06-10")
XCTAssertEqual(LifeDateFormat.storedValue(from: "jun 10 2026"), "2026-06-10")
XCTAssertEqual(LifeDateFormat.storedValue(from: "1990-02-03"), "1990-02-03")
XCTAssertNil(LifeDateFormat.storedValue(from: "Feb 31, 2026"))
XCTAssertNil(LifeDateFormat.storedValue(from: "someday"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct LifeSidebarView: View {
@State private var birthDate = ""
@State private var lifespan = ""
@State private var invalidBirthDate = false
/// Web `LifeQuote`: one random quote per visit to the sidebar.
@State private var quote = LifeQuotes.random()
@FocusState private var focusedField: Field?
private enum Field { case birthDate, lifespan }

Expand Down Expand Up @@ -38,7 +40,7 @@ struct LifeSidebarView: View {
.accessibilityLabel("Date of birth")
.focused($focusedField, equals: .birthDate)
.onSubmit { commitBirthDate() }
.help("Enter a date as yyyy-mm-dd")
.help("Enter a date like Jun 10, 2026")
.padding(.vertical, 8).frame(width: 176)
.background(theme.surfaceColor).foregroundStyle(theme.textColor)
.clipShape(RoundedRectangle(cornerRadius: 4))
Expand Down Expand Up @@ -69,7 +71,7 @@ struct LifeSidebarView: View {
}
.textFieldStyle(.plain)
.multilineTextAlignment(.center)
Text("“Lost time is never found again.” (Benjamin Franklin)").italic()
Text(quote).italic()
}
.font(.rubik(14))
.foregroundStyle(theme.textMutedColor)
Expand Down Expand Up @@ -97,12 +99,15 @@ struct LifeSidebarView: View {
}

private func refreshInputs() {
birthDate = model.lifeStore.preferences.birthDate
birthDate = LifeDateFormat.display(model.lifeStore.preferences.birthDate)
lifespan = String(model.lifeStore.preferences.lifespan)
}

private func commitBirthDate() {
invalidBirthDate = !model.lifeStore.updateBirthDate(birthDate)
let typed = birthDate.trimmingCharacters(in: .whitespacesAndNewlines)
// An empty field clears the date; otherwise accept "Jun 10, 2026" or "2026-06-10".
invalidBirthDate = !model.lifeStore.updateBirthDate(
typed.isEmpty ? "" : (LifeDateFormat.storedValue(from: typed) ?? typed))
refreshInputs()
}

Expand Down
Loading