Created
February 20, 2022 15:13
-
-
Save helje5/a2548be20e4685b4be660d6afa847858 to your computer and use it in GitHub Desktop.
A completely incomplete implementation of `cal` using Swift / Foundation.Calendar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/swift | |
import Foundation | |
extension DateInterval { | |
func containsOpen(_ date: Date) -> Bool { date >= start && date < end } | |
} | |
struct MonthCalendar { | |
struct Day { | |
let openRange : DateInterval | |
let dayOfMonth : Int // 1-based | |
let dayOfWeek : Int // 1-based! (1=Sunday!) | |
let isHighlighted : Bool | |
let isInMonth : Bool | |
} | |
struct Week { | |
let openRange : DateInterval | |
let days : [ Day ] | |
} | |
let openRange : DateInterval // This is an OPEN range | |
let calendar : Calendar | |
let year : Int | |
let month : Int // 0-based | |
let highlight : Date? | |
let weeks : [ Week ] | |
init?(date: Date = Date(), highlight: Date? = Date(), | |
calendar: Calendar = .current) | |
{ | |
guard let monthRange = calendar.dateInterval(of: .month, for: date) else { | |
assertionFailure("Could not get range of month for date: \(date)") | |
return nil | |
} | |
guard let firstWeek = | |
calendar.dateInterval(of: .weekOfYear, for: monthRange.start) | |
else { | |
assertionFailure("Could not get first week for date: \(date)") | |
return nil | |
} | |
let components = calendar.dateComponents([.month, .year], from: date) | |
guard let month = components.month, let year = components.year else { | |
assertionFailure("Could not get month/year for date: \(date)") | |
return nil | |
} | |
func addDay(_ range: DateInterval, to days: inout [ Day ], | |
calendar: Calendar) | |
{ | |
let comps = calendar.dateComponents([.day, .weekday], from: range.start) | |
days.append(Day( | |
openRange : range, | |
dayOfMonth : comps.day ?? 0, dayOfWeek: comps.weekday ?? 0, | |
isHighlighted : highlight.flatMap { range.containsOpen($0) } ?? false, | |
isInMonth : monthRange.containsOpen(range.start) | |
)) | |
} | |
func addWeek(_ range: DateInterval, to weeks: inout [ Week ], | |
calendar: Calendar) | |
{ | |
var days = [ Day ](); days.reserveCapacity(7) | |
guard let firstDay = calendar.dateInterval(of: .day, for: range.start) | |
else { | |
assertionFailure("Could not get first day for date: \(date)") | |
return | |
} | |
addDay(firstDay, to: &days, calendar: calendar) | |
calendar.enumerateDates( | |
startingAfter: range.start, | |
matching: DateComponents(hour: 0, minute: 0, second: 0), // every day | |
matchingPolicy: .nextTime | |
) | |
{ date, exactMatch, stop in | |
guard let date = date else { stop = true; return } | |
guard date < range.end else { stop = true; return } | |
guard let range = calendar.dateInterval(of: .day, for: date) else { | |
assertionFailure("Could not get range of week: \(date)") | |
stop = true; return | |
} | |
addDay(range, to: &days, calendar: calendar) | |
} | |
weeks.append(Week(openRange: range, days: days)) | |
} | |
var weeks = [ Week ](); weeks.reserveCapacity(6) | |
addWeek(firstWeek, to: &weeks, calendar: calendar) | |
let firstWeekDayMatcher = DateComponents(hour: 0, minute: 0, second: 0, | |
weekday: calendar.firstWeekday) | |
calendar.enumerateDates( | |
startingAfter: monthRange.start, | |
matching: firstWeekDayMatcher, | |
matchingPolicy: .nextTime | |
) | |
{ date, exactMatch, stop in | |
guard let date = date else { stop = true; return } | |
guard date < monthRange.end else { stop = true; return } | |
guard let range = calendar.dateInterval(of: .weekOfYear, for: date) else { | |
assertionFailure("Could not get range of week: \(date)") | |
stop = true; return | |
} | |
addWeek(range, to: &weeks, calendar: calendar) | |
} | |
self.calendar = calendar | |
self.openRange = monthRange | |
self.weeks = weeks | |
self.month = month | |
self.year = year | |
self.highlight = highlight | |
} | |
} | |
func calendarTextLines(for monthCalendar: MonthCalendar) -> [ String ] { | |
let calendar = monthCalendar.calendar | |
var textLines = [ String ]() | |
var monthRow : String { | |
let width = calendar.shortWeekdaySymbols.count * 3 - 1 | |
let monthString = calendar.monthSymbols[monthCalendar.month - 1] + " " | |
+ String(monthCalendar.year) | |
guard monthString.count < width else { return monthString } | |
let padSpace = width - monthString.count | |
assert(padSpace > 0) | |
return String(repeating: " ", count: padSpace / 2) + monthString | |
} | |
// This is going to contain "Sunday" on Mo-based | |
var headerRow : String { | |
let evenShorterWeekdaySymbols = | |
calendar.shortWeekdaySymbols.map { String($0.prefix(2)) } | |
let firstDoW = calendar.firstWeekday // 1-based | |
let shiftDays = evenShorterWeekdaySymbols.prefix (firstDoW - 1) | |
let startDays = evenShorterWeekdaySymbols.dropFirst(firstDoW - 1) | |
return (startDays + shiftDays).joined(separator: " ") | |
} | |
textLines.append(monthRow) | |
textLines.append(headerRow) | |
for week in monthCalendar.weeks { | |
let weekRow = week.days.map { day in | |
guard day.isInMonth else { return " " } // do not include other months | |
let s = String(day.dayOfMonth) | |
let padded = s.count < 2 ? " " + s : s | |
if day.isHighlighted { | |
let esc = "\u{001B}[" | |
return "\(esc)7m" + padded + "\(esc)0m" | |
} | |
return padded | |
} | |
.joined(separator: " ") | |
textLines.append(weekRow) | |
} | |
return textLines | |
} | |
// MARK: - Options | |
struct Options { | |
let calendar = Calendar.current | |
let date : Date | |
var monthsToPrint = 1 | |
var highlightToday = true | |
init?(argv: [ String ]) { | |
var int1 : Int?, int2 : Int? | |
// TODO: cal supports "f" and "p" suffixes for follow/previous | |
for arg in argv.dropFirst() { | |
if arg.first == "-" { | |
switch arg { | |
case "-h": highlightToday = false | |
case "-m": // month | |
if int1 != nil { int2 = int1; int1 = nil } // will fill int1 next | |
default: | |
print("Unsupported option:", arg) | |
return nil | |
} | |
} | |
else if let i = Int(arg) { | |
if int1 == nil { int1 = i } | |
else if int2 == nil { int2 = i } | |
else { print("Unsupported option:", arg); return nil } | |
} | |
else { | |
print("Unsupported option:", arg) | |
return nil | |
} | |
} | |
switch ( int1, int2 ) { | |
case ( .none, _ ): | |
date = Date() | |
case ( .some(let year), .none ): | |
monthsToPrint = 12 | |
guard let date = calendar.date(from: DateComponents(year: year)) else { | |
print("Could not process year:", year) | |
return nil | |
} | |
self.date = date | |
case ( .some(let month), .some(let year) ): | |
guard let date = calendar.date(from: DateComponents(year: year, month: month)) | |
else { | |
print("Could not process month/year:", month, year) | |
return nil | |
} | |
self.date = date | |
} | |
} | |
} | |
// MARK: - MAIN | |
guard let options = Options(argv: CommandLine.arguments) else { | |
print("Could not parse options!") | |
exit(1) | |
} | |
guard let month = MonthCalendar(date: options.date, | |
highlight: options.highlightToday ? Date() : nil, | |
calendar: options.calendar) | |
else { | |
print("Could not produce calendar for date:", options.date) | |
exit(2) | |
} | |
for line in calendarTextLines(for: month) { | |
print(line) | |
} |
Author
helje5
commented
Feb 20, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment