Skip to content

Instantly share code, notes, and snippets.

@helje5
Created February 20, 2022 15:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save helje5/a2548be20e4685b4be660d6afa847858 to your computer and use it in GitHub Desktop.
Save helje5/a2548be20e4685b4be660d6afa847858 to your computer and use it in GitHub Desktop.
A completely incomplete implementation of `cal` using Swift / Foundation.Calendar
#!/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)
}
@helje5
Copy link
Author

helje5 commented Feb 20, 2022

$ ./cal.swift 
   February 2022
Mo Tu We Th Fr Sa Su
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28                  
$ ./cal.swift 2 2020
   February 2020
Mo Tu We Th Fr Sa Su
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment