Table of Contents

  1. Algorithm
  2. Review
    1. 表达并抛出错误
    2. 处理错误
    3. 使用抛出函数广播错误
    4. 使用 Do-Catch 处理错误
    5. 转换错误为可选值
    6. 禁止错误广播
    7. 指定错误类型
    8. 指定清理行为
  3. Tips
    1. 独立式的宏
    2. 依附宏
    3. 宏定义
    4. 宏扩展
    5. 实现一个宏
    6. 开发和调试宏
  4. Share
    1. 公式
    2. 分析

Algorithm

Leetcode 1187: Make Array Strictly Increasing

https://dreamume.medium.com/leetcode-1187-make-array-strictly-increasing-85d79f4ab498

Review

Error Handling

错误处理是在你的程序中响应进程和从错误条件中恢复。Swift 提供第一手类支持在运行时抛、捕获、广播和操作可恢复错误

一些操作不保证总是完成执行或产生一个有用的输出。可选项用来表达值的缺失,但当一个操作失败,它通常用来理解什么引起的错误,这样你的代码可相应地做出反应

作为一个例子,考虑一个从磁盘文件中读并处理数据的任务。有很多情况可导致任务失败,包括文件在指定路径下不存在,文件没有读取权限或文件没有用兼容的格式编码。区分这些不同的情形允许程序解决一些错误并通知用户它不能解决的错误

表达并抛出错误

在 Swift 中,错误被表示为服从 Error 协议的值类型。这个空协议表示一个类型可被用于错误处理

Swift 枚举是特别适合模型一组相关错误条件,关联值允许描述错误的额外信息。例如,这里显示在一个游戏中操作一个向量机器的错误条件:

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStack
}

抛出一个错误让你显示一些不期望发生的事情并且正常执行流不能继续。你使用 throw 表达式来抛出一个错误。例如,下面代码抛出一个错误显示向量机需要 5 个额外的硬币

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

处理错误

当一个错误抛出,一些代码必须响应来处理错误 - 例如,通过修正问题,尝试一个替代的处理,或通知用户失败

在 Swift 中有 4 种方法处理错误。你可广播一个函数的错误到调用该函数的函数中,用 do-catch 表达式处理错误,把错误作为一个可选值处理,或断言错误不会发生

当一个函数抛出一个错误,它改变了你的程序执行流,这样快速确认你的代码可抛出错误的地方是很重要的。为确定代码中的位置,用 try 关键字或 try? 或 try! 变种 - 在一段代码之前调用一个可抛出错误的函数、方法或初始化方法

使用抛出函数广播错误

为表示一个函数、方法或初始化函数可抛出一个错误,要在函数定义的参数之后写 throws 关键字。一个用 throws 标注的函数被称为一个抛出函数。如果函数指定一个特殊的返回类型,throws 关键字要在返回箭头之前

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

下面这个例子,VendingMachine 类有一个 vend(itemNamed:) 方法抛出一个适合的 VendingMachineError 如果请求项目无效,售罄或成本超过当前存款总额:

struct Item {
    var price: Int
    var count: Int
}


class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0


    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }


        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }


        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }


        coinsDeposited -= item.price


        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem


        print("Dispensing \(name)")
    }
}

因为 vend(itemNamed:) 方法广播它抛出的任何错误,调用这个方法的任何代码必须要么处理错误 - 使用一个 do-catch 表达式,try? 或 try! - 或继续广播它们。例如,下面例子中的 buyFavoriteSnack(person:vendingMachine:) 也是一个抛出函数,vend(itemNamed:) 方法抛出的任何错误将广播到 buyFavoriteSnack(person:vendingMachine:) 函数调用它的地方

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}

可抛出初始化函数可广播如同抛出函数一样的错误。例如,下面例子中 PurchasedSnack 结构的初始化函数调用一个可抛出函数,且它处理广播到它调用者的任意错误

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}

使用 Do-Catch 处理错误

通过运行一段代码你可使用 do-catch 表达式来处理错误。如果一个错误在 do 语句范围内被抛出,它会被对应的 catch 语句匹配并处理

下面是 do-catch 表达式的一般形式

do {
    try <#expression#>
    <#statements#>
} catch <#pattern 1#> {
    <#statements#>
} catch <#pattern 2#> where <#condition#> {
    <#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
    <#statements#>
} catch {
    <#statements#>
}

catch 不需要处理每个可能抛出的错误。如果没有对应的 catch 处理错误,错误广播到周围的代码。然而,广播的错误必须被某个周围的代码处理。在一个非抛出函数,一个 do-catch 表达式必须处理错误。在一个抛出函数,要么 do-catch 表达式处理了错误或调用函数必须处理错误。如果错误广播到顶层而没有被处理,你会得到一个运行时错误

示例代码如下:

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {
        print("Couldn't buy that from the vending machine.")
    }
}


do {
    try nourish(with: "Beet-Flavored Chips")
} catch {
    print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Couldn't buy that from the vending machine."

转换错误为可选值

你可以使用 try? 通过转换为一个可选值处理错误。如果一个错误在 try? 表达式中被抛出,表达式的值为 nil。例如,下面代码中 x 和 y 有相同的值和行为

func someThrowingFunction() throws -> Int {
    // ...
}


let x = try? someThrowingFunction()


let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

当你想要以相同的方式处理所有错误时使用 try? 让你写出健壮的错误处理代码。例如下面的代码:

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

禁止错误广播

有时你知道一个抛出函数或方法事实上不会在运行时抛出错误。在这种情况下,你可写 try! 禁止错误广播并封装调用在运行时断言中表示没有错误将会抛出。如果一个错误被抛出,你将得到一个运行时错误

示例如下

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

指定错误类型

上述所有的例子使用最常见的错误处理,代码中抛出的错误可为服从 Error 协议的任意类型值。这个处理符合当代码运行时实际上你不能提前知道每个发生的错误,特别当从其他地方广播的错误。它也反映事实即错误可在任何时间变化。一个库的新版本 - 包含你依赖使用的库 - 可抛出新的错误,且实际中用户配置的广大复杂度可暴露失败模式在开发或测试时不可见。上述例子中的错误处理代码总是包含一个缺省的例子来处理错误不需要一个特别的 catch 语句

大多数 Swift 代码不指明抛出错误的类型。然而,在下面特别的情况下你可限制代码只抛出一个特别的错误类型:

  • 当在一个嵌入式系统上运行代码不支持动态分配内存。抛出任意 Error 或其他封装协议类型的实例需要在运行时分配内存存储错误。相反的,抛出一个特定类型的错误使得 Swift 避免对错误进行堆分配
  • 当一个错误是某个代码单位实现细节,比如一个库,且不是代码接口的部分。因为错误只来自于库,且不是从其他依赖或库的客户端代码,你可制作一个所有可能失败的全面列表。且因为这些错误是库的实现细节,总是在库里处理
  • 只广播被一般性参数描述的错误的代码,比如一个函数带一个闭包参数和广播该闭包的任意错误

例如,考虑代码总结评价和使用以下错误类型

enum StatisticsError: Error {
    case noRatings
    case invalidRating(Int)
}

为指定一个函数只抛出 StatisticsError 值作为错误,你在定义函数时写 throws(StatisticsError)。这个语法也称为类型抛出。例如,下面函数抛出 StatisticsError 值作为它的错误

func summarize(_ ratings: [Int]) throws(StatisticsError) {
    guard !ratings.isEmpty else { throw .noRatings }


    var counts = [1: 0, 2: 0, 3: 0]
    for rating in ratings {
        guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
        counts[rating]! += 1
    }


    print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}

你可在一个常规抛出函数中调用一个使用类型抛出的函数

func someThrowingFunction() -> throws {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

其等同于

func someThrowingFunction() -> throws(any Error) {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

你可写一个函数不会抛出错误用 throws(Never)

func nonThrowingFunction() throws(Never) {
  // ...
}

你也可在 do-catch 表达式中指明错误类型

let ratings = []
do throws(StatisticsError) {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available"

指定清理行为

使用一个 defer 表达式执行一系列行为在代码离开当前代码块之前。这个表述让你做任意清理处理不管代码如何执行离开当前代码块 - 是否它离开因为抛出了一个错误或因为 return 或 break 导致

多个 defer 表述以相反的顺序执行,即第一个 defer 表述会最后执行

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // Work with the file.
        }
        // close(file) is called here, at the end of the scope.
    }
}

Tips

Macros

宏在编译时转换你的源代码,避免手写重复的代码。在编译时,Swift 扩展代码里的所有宏

扩展一个宏总是一个附加的操作:宏添加新的代码,但它们不修改或删除现存的代码

宏的输入和输出都会被检查来确保是有效的 Swift 代码。传递给宏的值和宏生成代码的值都会被检查确保它们有正确的类型。另外,如果宏的实现在宏扩展时遇到一个错误,编译器把它作为一个编译错误。这些确保了使用宏的代码更容易推理,且使得宏有错误或宏实现有 bug 时更容易定位

Swift 有两种宏:

  • 独立式的宏,不需要依附于一个定义
  • 依附式的宏修改它依附的定义

你调用依附和独立式的宏有一些不一样,但它们都服从宏扩展的相同模型,且你实现都使用相同的处理

独立式的宏

调用一个独立式的宏,在名字前写一个 # 号,且在名字后括号里写任意参数

func myFunction() {
    print("Currently running \(#function)")
    #warning("Something's wrong")
}

独立式的宏可产生一个值,如 #function,或在编译时可执行一个行为,如 #warning

依附宏

调用一个依附宏,在名字前写一个 @ 号,且在名字后括号里写任意参数

依附宏修改它依附的定义。添加代码到定义中,就像定义一个新方法或服从一个协议

例如,考虑如下不使用宏的代码:

struct SundaeToppings: OptionSet {
    let rawValue: Int
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

代码中每个 SundaeToppings 选项有一个初始化函数调用

下面是使用宏的代码版本:

@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

这个版本的 Sundaetoppings 调用一个 @OptionSet 宏。该宏读取私有枚举项目列表,对每个选项产生常量列表,且服从 OptionSet 协议

为比较,下面是 @OptionSet 宏的扩展版本,你不需要写这个代码,且只有你指明要 Swift 显示宏扩展时你会看到

struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }


    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppings: OptionSet { }

宏定义

在多数 Swift 代码里,当你实现一个符号,比如一个函数或类型,是没有独立的定义的。然而,对宏,定义和实现是分开的。一个宏的定义包含它的名字,参数和它产生的代码种类。一个宏的实现包含通过产生 Swift 代码的宏扩展

用 macro 关键字引入一个宏定义。例如,下面是之前例子中 @OptionSet 宏的部分定义:

public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

第一行指明宏的名字和参数 - 名字是 OptionSet,不带任何参数。第二行使用 Swift 标准库里的 externalMacro(module:type:) 宏来告诉 Swift 宏实现的位置。这个例子中,SwiftMacros 宏包含一个类型名 OptionSetMacro,其实现 @OptionSet 宏

因为 OptionSet 是一个依附宏,它的名字使用大写骆驼写法,跟结构和类的名字相似。独立式宏有小写的骆驼写法名,跟变量和函数相似

注意:宏总是定义为 public。因为定义一个宏的代码在不同的模块,这样不能用 nonpublic 的宏

一个宏定义定义宏的角色 - 在源代码中宏可被调用的位置,和宏可产生的代码类型。每个宏有一个或多个角色,在宏定义的开头写上属性。下面是 @OptionSet 更详细的定义,包含它的角色的属性

@attached(member)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

@attached 属性在这个定义中出现了两次,每个对应一个宏角色。第一个,@attached(member) 表示宏添加新的成员到类型。@OptionSet 宏添加一个 OptionSet 协议需要的 init(rawValue:) 初始化函数,及一些额外的成员。第二个,@attached(extension, conformances: OptionSet) 表示 @OptionSet 服从 OptionSet 协议。@OptionSet 宏扩展类型服从 OptionSet 协议

对独立式宏,用 @freestanding 属性指明它的角色

@freestanding(expression)
public macro line<T: ExpressibleByIntegerLiteral>() -> T =
        /* ... location of the macro implementation... */

#line 宏有一个 expression 角色。一个 expression 宏产生一个值,或执行一个编译期行为比如产生一个警告

对于宏角色,一个宏的定义提供宏产生的符号名字信息。当一个宏定义提供一个名字列表,它确保产生只使用这些名字的定义,帮助你理解和调试产生的代码。下面是 @OptionSet 的完整定义

@attached(member, names: named(RawValue), named(rawValue),
        named(`init`), arbitrary)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() =
        #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

@attached(member) 宏包含参数,在 named 里面:每个标签对应 @OptionSet 宏产生的每个符号。这个宏添加 RawValue、rawValue、init 符号的定义 - 因为这些名字提前知道,宏定义直接列出来

宏定义也包含名字列表后的 arbitrary,允许宏产生这些名字不知道的定义直到使用宏时。例如,当 @OptionSet 宏应用到之前例子中的 SundaeToppings,它产生类型属性对应枚举值,nuts、cherry 和 fudge

宏扩展

当使用宏构建 Swift 代码,编译期调用宏实现来扩展它们

特别地,Swift 用如下方式扩展宏:

  1. 编译器读代码,创建语法的内存表示
  2. 编译器发送部分内存表示到宏实现,扩展宏
  3. 编译器用它的扩展形式替换宏调用
  4. 编译器使用扩展源代码继续编译

考虑下面这个情况:

let magicNumber = #fourCharacterCode("ABCD")

#fourCharacterCode 宏带一个四字符长的字符串并转换为字符对应 ASCII 码并连接一起的 32 位无符号整数。一些文件格式使用整型来确认数据因为它们兼容但在调试器中依然可读

为展开上述代码中的宏,编译器读取 Swift 文件并创建代码的内存表示即抽象语法树,或 AST。AST 直接生成代码的结构,使其更容易写代码交互 - 就像一个编译器或宏实现。下面是上述代码的 AST 呈现,为简化省略了一些额外的细节

img

上述图形显示该代码在内存中的结构呈现。AST 中每个元素对应源代码的一部分。Swift 通过限制实现宏的代码帮助宏作者避免误读取其他输入

  • 宏实现的 AST 只包含表示宏的 AST 元素,没有其他代码
  • 宏实现运行在沙箱环境中防止它访问文件系统或网络

除了这些保护措施,宏作者要负责不读取或修改宏输入外的任何事物。例如,一个宏扩展必须不依赖当前时间

#fourCharacterCode 的实现产生一个新的 AST 包含扩展代码,下面是返回给编译器的代码

img

当编译器收到扩展,它替换包含宏调用的 AST 元素到包含宏扩展的元素。在宏扩展之后,编译器再检查确保程序还是有效的 Swift 代码且类型是正确的。其产生一个最终的 AST 可被编译:

img

该 AST 对应的 SWif 代码如下:

let magicNumber = 1145258561 as UInt32

在这个例子中,输入源代码只有一个宏,但实际程序有宏的几个实例且几个到其他宏的调用。编译器同时扩展宏

如果一个宏出现在另一个内部,外层的宏先扩展 - 使得外层的宏在它扩展之前修改内部的宏

实现一个宏

为实现一个宏:你要做两个部分:一个类型执行宏扩展,一个库定义宏来暴露它作为 API。这些部分在使用宏时从代码中分别构建,甚至你同时开发宏和它的客户端,因为宏实现作为构建宏的客户端的一部分运行

为使用 Swift Package Manager 创建一个新宏,运行 swift package init –type macro - 这创建几个文件,包括宏实现和定义的一个模版

为添加宏到现存的工程,编辑你的 Package.swift 文件开头如下:

  • 在 swift-tools-version 中设置 Swift 工具版本为 5.9 或更新
  • 导入 CompilerPluginSupport 模块
  • 包含 macOS 10.15 最为最小开发平台列表

下面代码显示一个 Package.swift 的例子的开头

// swift-tools-version: 5.9


import PackageDescription
import CompilerPluginSupport


let package = Package(
    name: "MyPackage",
    platforms: [ .iOS(.v17), .macOS(.v13)],
    // ...
)

接着,添加宏实现的一个目标且一个宏库的目标到你现存 Package.swift 文件。例如,你可添加类似如下代码,改变名字来匹配你的工程

targets: [
    // Macro implementation that performs the source transformations.
    .macro(
        name: "MyProjectMacros",
        dependencies: [
            .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
            .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
        ]
    ),


    // Library that exposes a macro as part of its API.
    .target(name: "MyProject", dependencies: ["MyProjectMacros"]),
]

上面代码定义两个目标:MyProjectMacros 包含宏实现,MyProject 使得这些宏有效

宏实现使用 SwiftSyntax 模块使用一个 AST 以结构性的方式来交互 Swift 代码。如果你用 Swift Package Manager 创建一个新的宏包,生成的 Package.swift 文件自动包含一个对 SwiftSyntax 的依赖。如果添加宏到现存的工程,在你的 Package.swift 文件中添加一个 SwiftSyntax 的依赖

dependencies: [
    .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0")
],

依赖于你的宏的角色,有一个 SwiftSyntax 对应的协议宏实现要服从。例如,考虑前面章节的 #fourCharacterCode。下面是实现该宏的结构:

import SwiftSyntax
import SwiftSyntaxMacros


public struct FourCharacterCode: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression,
              let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
              segments.count == 1,
              case .stringSegment(let literalSegment)? = segments.first
        else {
            throw CustomError.message("Need a static string")
        }


        let string = literalSegment.content.text
        guard let result = fourCharacterCode(for: string) else {
            throw CustomError.message("Invalid four-character code")
        }


        return "\(raw: result) as UInt32"
    }
}


private func fourCharacterCode(for characters: String) -> UInt32? {
    guard characters.count == 4 else { return nil }


    var result: UInt32 = 0
    for character in characters {
        result = result << 8
        guard let asciiValue = character.asciiValue else { return nil }
        result += UInt32(asciiValue)
    }
    return result
}
enum CustomError: Error { case message(String) }

如果你添加这个宏到一个现存的 Swift Package Manager 工程,添加一个类型作为宏目标的条目点并列出目标定义的宏:

import SwiftCompilerPlugin

@main
struct MyProjectMacros: CompilerPlugin {
    var providingMacros: [Macro.Type] = [FourCharacterCode.self]
}

#fourCharacterCode 宏是一个独立式宏产生一个表达式,所以 FourCharacterCode 类型的实现服从 ExpressionMacro 协议。ExpressionMacro 协议有一个需求,一个 expansion(of:in:) 方法扩展 AST

为扩展 #fourCharacterCode 宏,Swift 发送使用这个宏的代码的 AST 到包含宏实现的库。在库内部,Swift 调用 FourCharacterCode.expansion(of:in:),在 AST 中传递并把上下文作为参数。expansion(of:in:) 的实现找到作为参数传递的字符串并计算对应的 32 位无符号整型值

上例中,第一个保护块从 AST 中提取字符串,赋值到 AST 元素 literalSegment。第二个保护块调用私有的 fourCharacterCode(for:) 函数。如果宏错误使用这些块会抛出错误 - 错误信息变成一个编译器错误

expansion(of:in:) 方法返回一个 ExprSyntax 的实例,SwiftSyntax 里的一个类型在 AST 中表示一个表达式。因为类型服从 StringLiteralConvertible 协议,宏实现使用字符串常量值作为一个轻量的语法来创建它的结果。所有从宏实现返回的所有 SwiftSyntax 类型服从 StringLiteralConvertible 协议,这样当实现任何类型宏时你都可使用这个处理

开发和调试宏

宏很适合开发时测试:它们转换一个 AST 到另一个 AST 而不依赖任何外部的状态,且不改变任何外部状态。另外,你可从字符串常量中创建语法节点,其简化输入构建来测试。你也可读 AST 的 description 属性来获得一个字符串来和期望值比较。例如,下面是一个对 #fourCharacterCode 宏的测试

let source: SourceFileSyntax =
    """
    let abcd = #fourCharacterCode("ABCD")
    """


let file = BasicMacroExpansionContext.KnownSourceFile(
    moduleName: "MyModule",
    fullFilePath: "test.swift"
)


let context = BasicMacroExpansionContext(sourceFiles: [source: file])


let transformedSF = source.expand(
    macros:["fourCharacterCode": FourCharacterCode.self],
    in: context
)


let expectedDescription =
    """
    let abcd = 1145258561 as UInt32
    """


precondition(transformedSF.description == expectedDescription)

Share

Zeller’s congruence

Zeller 的同构是 Christian Zeller 在 19 世纪发明的算法计算儒略历或公历日期对应的周几。它基于儒略历和公历日期的转换

公式

对公历,Zeller 的同构为

$ h = \left( q + \lfloor \frac{13(m+1)}{5} \rfloor + K + \lfloor \frac{K}{4} \rfloor + \lfloor \frac{J}{4} \rfloor - 2J \right) \bmod 7 $

对儒略历为

$ h = \left( q + \lfloor \frac{13(m+1)}{5} \rfloor + K + \lfloor \frac{K}{4} \rfloor + 5 - J \right) \bmod 7 $

  • h 是周几(0 = 周六,1 = 周日,2 = 周一, …, 6 = 周五)
  • q 是月份中的日期
  • m 是月份(3 = 三月,4 = 四月, …, 14 = 二月)
  • K 是年份对 100 求余
  • J 是年份除以 100 取下限
  • $ \lfloor \ldots \rfloor $ 是整数取下限
  • mod 是求余操作

注意在这个算法中一月和二月被作为前一年的第 13 和 14 月。例如,2010 年 2 月 2 号,算法会视为 2009 年第 14 月第 2 天

对 ISO 周日期(1 = 周一到 7 = 周日),使用

$ d = ( ( h + 5 ) \bmod 7) + 1 $

分析

这些公式基于在基于日期的每个子部分的预测方法上周日期的观察。公式的每项用于计算需要获得获得正确周日期的偏移

对公历,这个公式的各个部分因此理解如下:

  • q 代表基于月份上天数的周日期,因为每个持续日结果在周日期上有一个额外的 1 的偏移
  • K 代表基于年份的周的日期的处理。假设每年是 365 天,每个后续年的相同日期上将有一个 365 mod 7 = 1 的偏移
  • 因为每个闰年有 366 天,这需要通过在周的日期上加一个 1 天的偏移值。这通过添加 $ \lfloor \frac{K}{4} \rfloor $ 的偏移实现
  • 使用相似的逻辑,对每个世纪周日期的处理通过观察在正常的世纪里有 36524 天和每个能被 400 整除的世纪里有 36525 天。因为 36525 mod 7 = 6 和 36524 mod 7 = 5,$ \lfloor \frac{J}{4} \rfloor - 2 J $ 就是计算的这个
  • 项目 $ \lfloor \frac{13(m+1)}{5} \rfloor $ 为月份中天数的变种。从一月开始,每月的天数为 31, 28/29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31。二月比较特殊,所以公式把一月和二月移到末尾这样二月的短缺就不会引起问题。公式要求周几,这样序列中的数可对 7 取模。这样每月天数对 7 取模后的值为 3,0/1, 3, 2, 3, 2, 3, 3, 2, 3, 2, 3。从三月开始,序列交替为 3, 2, 3, 2, 3,但每 5 月有 2 个 31 天数的月份。分数 13/5 = 2.6 且取下值函数起了作用;除数 5 使得 5 月作为一个周期
  • 整个函数对 7 取模,即为每周对应的日期

不同日历间公式的不同是因为儒略历对润世纪没有独立的规则,且是通过每个世纪固定的天数从公历计算偏移