marr

package module
v1.0.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

marr - 数组/集合操作组件

封装数组比较、集合操作等功能,支持查询两个数组的重合部分和不重合部分。

特性

  • 🔍 数组比较:求交集、差集、并集、对称差集
  • 📊 详细统计:提供比较结果的详细统计信息
  • 🔄 保持顺序:可选择是否保持原数组顺序
  • 🎯 泛型支持:支持任意可比较类型的数组
  • 🛠️ 结构体支持:通过键提取函数支持结构体数组比较
  • 📦 集合操作:完整的集合数据结构及操作

快速开始

基础数组比较
import "admin_server/component/marr"

a := []int{1, 2, 3, 4, 5}
b := []int{4, 5, 6, 7, 8}

// 完整比较,获取所有信息
result := marr.Compare(a, b)

fmt.Println(result.Intersection)        // [4 5] - 重合部分
fmt.Println(result.OnlyInA)             // [1 2 3] - 只在 A 中
fmt.Println(result.OnlyInB)             // [6 7 8] - 只在 B 中
fmt.Println(result.Union)               // [1 2 3 4 5 6 7 8] - 并集
fmt.Println(result.SymmetricDifference) // [1 2 3 6 7 8] - 对称差集

// 查看统计信息
fmt.Println(result.Stats.TotalA)             // 5
fmt.Println(result.Stats.IntersectionCount)  // 2
fmt.Println(result.Stats.DuplicateCountA)    // 0
保持原数组顺序
a := []int{5, 4, 3, 2, 1}
b := []int{8, 7, 6, 5, 4}

// 第三个参数 true 表示保持原数组顺序
result := marr.Compare(a, b, true)

// 结果保持 A 数组的顺序
fmt.Println(result.Intersection) // [5 4] (不是 [4 5])
fmt.Println(result.OnlyInA)      // [3 2 1]
便捷函数
a := []int{1, 2, 3, 4, 5}
b := []int{4, 5, 6, 7, 8}

// 只获取交集
intersection := marr.Intersection(a, b) // [4 5]

// 只获取差集(在 a 中但不在 b 中)
diff := marr.Difference(a, b) // [1 2 3]

// 双向差集
onlyInA, onlyInB := marr.DifferenceBoth(a, b)

// 并集
union := marr.Union(a, b)

// 对称差集
symDiff := marr.SymmetricDifference(a, b)

// 判断是否有交集
hasInter := marr.HasIntersection(a, b) // true

// 判断是否为子集
isSubset := marr.IsSubset([]int{1, 2}, []int{1, 2, 3}) // true
数组去重
arr := []int{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}

// 去重(不保证顺序)
unique := marr.Unique(arr) // [1 2 3 4]

// 去重并保持原数组顺序
uniqueOrdered := marr.Unique(arr, true) // [1 2 3 4]

结构体数组比较

对于结构体数组,可以通过键提取函数进行比较:

type User struct {
    ID   int
    Name string
    Age  int
}

usersA := []User{
    {ID: 1, Name: "Alice", Age: 25},
    {ID: 2, Name: "Bob", Age: 30},
    {ID: 3, Name: "Charlie", Age: 35},
}

usersB := []User{
    {ID: 2, Name: "Bob", Age: 30},
    {ID: 3, Name: "Charlie", Age: 35},
    {ID: 4, Name: "David", Age: 40},
}

// 使用 ID 作为比较键
result := marr.CompareByKey(usersA, usersB, func(u User) int { return u.ID })

fmt.Println(result.Intersection) // [{ID:2 Name:Bob Age:30} {ID:3 Name:Charlie Age:35}]
fmt.Println(result.OnlyInA)      // [{ID:1 Name:Alice Age:25}]
fmt.Println(result.OnlyInB)      // [{ID:4 Name:David Age:40}]
其他结构体操作
// 根据字段去重
uniqueUsers := marr.UniqueByKey(users, func(u User) int { return u.ID })

// 分组
usersByAge := marr.GroupBy(users, func(u User) int { return u.Age })
// 返回: map[int][]User

// 转换为映射
userMap := marr.ToMap(users, func(u User) int { return u.ID })
// 返回: map[int]User

集合操作

// 创建集合
s1 := marr.NewSetFromSlice([]int{1, 2, 3, 4, 5})
s2 := marr.NewSetFromSlice([]int{4, 5, 6, 7, 8})

// 基本操作
s1.Add(6)
s1.Remove(1)
exists := s1.Contains(2) // true

// 集合运算
intersection := s1.Intersection(s2)
difference := s1.Difference(s2)
union := s1.Union(s2)
symDiff := s1.SymmetricDifference(s2)

// 判断关系
isSubset := s1.IsSubset(s2)
isSuperset := s1.IsSuperset(s2)
isEqual := s1.Equal(s2)

// 转换为切片
slice := s1.ToSlice()

API 文档

数组比较函数
函数 说明
Compare(a, b []T, keepOrder ...bool) *CompareResult[T] 完整比较两个数组
Intersection(a, b []T, keepOrder ...bool) []T 求交集(重合部分)
Difference(a, b []T, keepOrder ...bool) []T 求差集(只在 A 中)
DifferenceBoth(a, b []T, keepOrder ...bool) ([]T, []T) 双向差集
Union(a, b []T, keepOrder ...bool) []T 求并集
SymmetricDifference(a, b []T, keepOrder ...bool) []T 对称差集
HasIntersection(a, b []T) bool 判断是否有交集
IsSubset(a, b []T) bool 判断 A 是否为 B 的子集
IsSuperset(a, b []T) bool 判断 A 是否为 B 的超集
Equal(a, b []T) bool 判断两数组元素是否相同(不考虑顺序和重复)
EqualStrict(a, b []T) bool 严格判断(考虑顺序)
Unique(arr []T, keepOrder ...bool) []T 数组去重
SortAndCompare(a, b []T) *CompareResult[T] 排序后比较
CompareResult 结构
type CompareResult[T comparable] struct {
    Intersection        []T          // 重合部分
    OnlyInA             []T          // 只在 A 中
    OnlyInB             []T          // 只在 B 中
    Union               []T          // 并集
    SymmetricDifference []T          // 对称差集
    Stats               CompareStats // 统计信息
}

type CompareStats struct {
    TotalA             int // A 数组总数
    TotalB             int // B 数组总数
    IntersectionCount  int // 重合数量
    OnlyInACount       int // 只在 A 中的数量
    OnlyInBCount       int // 只在 B 中的数量
    UnionCount         int // 并集数量
    SymmetricDiffCount int // 对称差集数量
    DuplicateCountA    int // A 中重复元素数量
    DuplicateCountB    int // B 中重复元素数量
}
结构体数组函数
函数 说明
CompareByKey(a, b []T, keyFn KeyFunc[T,K], keepOrder ...bool) *CompareResultFunc[T] 使用键函数比较
IntersectionByKey(a, b []T, keyFn KeyFunc[T,K], keepOrder ...bool) []T 求交集
DifferenceByKey(a, b []T, keyFn KeyFunc[T,K], keepOrder ...bool) []T 求差集
UniqueByKey(arr []T, keyFn KeyFunc[T,K], keepOrder ...bool) []T 根据键去重
GroupBy(arr []T, keyFn KeyFunc[T,K]) map[K][]T 分组
ToMap(arr []T, keyFn KeyFunc[T,K]) map[K]T 转换为映射
集合方法
方法 说明
NewSet[T]() 创建新集合
NewSetFromSlice(slice []T) 从切片创建集合
Add(v T) 添加元素
Remove(v T) 删除元素
Contains(v T) bool 判断是否包含
Size() int 返回大小
IsEmpty() bool 判断是否为空
ToSlice() []T 转换为切片
Union(other *Set[T]) *Set[T] 并集
Intersection(other *Set[T]) *Set[T] 交集
Difference(other *Set[T]) *Set[T] 差集
SymmetricDifference(other *Set[T]) *Set[T] 对称差集
Equal(other *Set[T]) bool 判断是否相等
IsSubset(other *Set[T]) bool 判断是否为子集
IsSuperset(other *Set[T]) bool 判断是否为超集
Clone() *Set[T] 克隆集合

测试

cd admin_server/component/marr
go test -v

# 基准测试
go test -bench=.

注意事项

  1. 重复元素:默认情况下会去除重复元素,统计信息中包含重复元素数量
  2. 顺序控制:大部分函数支持 keepOrder 参数控制是否保持原数组顺序
  3. 性能:使用 Set 实现,时间复杂度为 O(n),适合大数据量
  4. 泛型约束:基础类型需要满足 comparable 约束

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Difference

func Difference[T comparable](a, b []T, keepOrder ...bool) []T

Difference 求数组差集(在 a 中但不在 b 中的元素)

Example

ExampleDifference 求差集示例

a := []int{1, 2, 3, 4, 5}
b := []int{4, 5, 6, 7, 8}

// 在 A 中但不在 B 中的元素(保持顺序)
onlyInA := Difference(a, b, true)

// 双向差集(保持顺序)
onlyInA2, onlyInB := DifferenceBoth(a, b, true)

fmt.Printf("只在 A 中: %v\n", onlyInA)
fmt.Printf("双向差集 - 只在 A: %v, 只在 B: %v\n", onlyInA2, onlyInB)
Output:
只在 A 中: [1 2 3]
双向差集 - 只在 A: [1 2 3], 只在 B: [6 7 8]

func DifferenceBoth

func DifferenceBoth[T comparable](a, b []T, keepOrder ...bool) (onlyInA, onlyInB []T)

DifferenceBoth 求双向差集(返回只在 a 中的和只在 b 中的)

func DifferenceBothByKey

func DifferenceBothByKey[T any, K comparable](a, b []T, keyFn KeyFunc[T, K], keepOrder ...bool) (onlyInA, onlyInB []T)

DifferenceBothByKey 使用键提取函数求双向差集

func DifferenceByKey

func DifferenceByKey[T any, K comparable](a, b []T, keyFn KeyFunc[T, K], keepOrder ...bool) []T

DifferenceByKey 使用键提取函数求差集

func Equal

func Equal[T comparable](a, b []T) bool

Equal 判断两个数组包含的元素是否相同(不考虑顺序和重复)

func EqualStrict

func EqualStrict[T comparable](a, b []T) bool

EqualStrict 严格判断两个数组是否相同(考虑顺序,不考虑重复)

func GroupBy

func GroupBy[T any, K comparable](arr []T, keyFn KeyFunc[T, K]) map[K][]T

GroupBy 根据键函数对数组进行分组

Example

ExampleGroupBy 分组示例

type Product struct {
	Category string
	Name     string
	Price    float64
}

products := []Product{
	{Category: "水果", Name: "苹果", Price: 5.0},
	{Category: "水果", Name: "香蕉", Price: 3.0},
	{Category: "蔬菜", Name: "西红柿", Price: 4.0},
	{Category: "蔬菜", Name: "黄瓜", Price: 2.5},
}

// 按类别分组
grouped := GroupBy(products, func(p Product) string { return p.Category })

fmt.Printf("水果类商品数量: %d\n", len(grouped["水果"]))
fmt.Printf("蔬菜类商品数量: %d\n", len(grouped["蔬菜"]))
Output:
水果类商品数量: 2
蔬菜类商品数量: 2

func HasIntersection

func HasIntersection[T comparable](a, b []T) bool

HasIntersection 判断两个数组是否有交集

func Intersection

func Intersection[T comparable](a, b []T, keepOrder ...bool) []T

Intersection 求数组交集(重合部分)

Example

ExampleIntersection 求交集示例

a := []string{"apple", "banana", "cherry", "date"}
b := []string{"banana", "date", "fig", "grape"}

// 使用 keepOrder=true 保持 A 数组的顺序
result := Intersection(a, b, true)

fmt.Printf("数组 A: %v\n", a)
fmt.Printf("数组 B: %v\n", b)
fmt.Printf("交集: %v\n", result)
Output:
数组 A: [apple banana cherry date]
数组 B: [banana date fig grape]
交集: [banana date]

func IntersectionByKey

func IntersectionByKey[T any, K comparable](a, b []T, keyFn KeyFunc[T, K], keepOrder ...bool) []T

IntersectionByKey 使用键提取函数求交集

func IsSubset

func IsSubset[T comparable](a, b []T) bool

IsSubset 判断 a 是否为 b 的子集

func IsSuperset

func IsSuperset[T comparable](a, b []T) bool

IsSuperset 判断 a 是否为 b 的超集

func SymmetricDifference

func SymmetricDifference[T comparable](a, b []T, keepOrder ...bool) []T

SymmetricDifference 求对称差集(只在其中一个数组中的元素)

func ToMap

func ToMap[T any, K comparable](arr []T, keyFn KeyFunc[T, K]) map[K]T

ToMap 将数组转换为映射(键函数返回的键 -> 值) 如果数组中有重复键,后面的值会覆盖前面的值

Example

ExampleToMap 转换为映射示例

type User struct {
	ID   int
	Name string
}

users := []User{
	{ID: 1, Name: "Alice"},
	{ID: 2, Name: "Bob"},
	{ID: 3, Name: "Charlie"},
}

// 转换为以 ID 为键的映射
userMap := ToMap(users, func(u User) int { return u.ID })

fmt.Printf("ID=1 的用户: %v\n", userMap[1])
fmt.Printf("ID=2 的用户: %v\n", userMap[2])

func ToMapWithMerge

func ToMapWithMerge[T any, K comparable](arr []T, keyFn KeyFunc[T, K], mergeFn func(existing, new T) T) map[K]T

ToMapWithMerge 将数组转换为映射,对于重复键使用合并函数处理

func Union

func Union[T comparable](a, b []T, keepOrder ...bool) []T

Union 求数组并集

func UnionByKey

func UnionByKey[T any, K comparable](a, b []T, keyFn KeyFunc[T, K], keepOrder ...bool) []T

UnionByKey 使用键提取函数求并集

func Unique

func Unique[T comparable](arr []T, keepOrder ...bool) []T

Unique 数组去重

Example

ExampleUnique 数组去重示例

arr := []int{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}

// 保持顺序(推荐,结果可预期)
result := Unique(arr, true)

fmt.Printf("原数组: %v\n", arr)
fmt.Printf("去重后: %v\n", result)
Output:
原数组: [1 2 2 3 3 3 4 4 4 4]
去重后: [1 2 3 4]

func UniqueByKey

func UniqueByKey[T any, K comparable](arr []T, keyFn KeyFunc[T, K], keepOrder ...bool) []T

UniqueByKey 使用键提取函数去重

Types

type Comparable

type Comparable interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~string
}

Comparable 可比较类型约束(用于排序)

type CompareFunc

type CompareFunc[T any] func(a, b T) bool

CompareFunc 自定义比较函数类型

type CompareResult

type CompareResult[T comparable] struct {
	// Intersection 重合部分(两个数组都有的元素)
	Intersection []T `json:"intersection"`
	// OnlyInA 只在 A 中的元素
	OnlyInA []T `json:"only_in_a"`
	// OnlyInB 只在 B 中的元素
	OnlyInB []T `json:"only_in_b"`
	// Union 并集(所有元素去重)
	Union []T `json:"union"`
	// SymmetricDifference 对称差集(只在其中一个数组中的元素)
	SymmetricDifference []T `json:"symmetric_difference"`
	// Stats 统计信息
	Stats CompareStats `json:"stats"`
}

CompareResult 数组比较结果

Example (Stats)

ExampleCompareResult_stats 查看比较统计信息示例

a := []int{1, 1, 2, 2, 3, 3, 4, 5} // 有重复元素
b := []int{4, 4, 5, 5, 6, 6, 7, 8}

result := Compare(a, b)

fmt.Printf("数组 A 总数: %d\n", result.Stats.TotalA)
fmt.Printf("数组 B 总数: %d\n", result.Stats.TotalB)
fmt.Printf("数组 A 去重后: %d\n", result.Stats.TotalA-result.Stats.DuplicateCountA)
fmt.Printf("数组 B 去重后: %d\n", result.Stats.TotalB-result.Stats.DuplicateCountB)
fmt.Printf("重合数量: %d\n", result.Stats.IntersectionCount)
fmt.Printf("只在 A 中: %d\n", result.Stats.OnlyInACount)
fmt.Printf("只在 B 中: %d\n", result.Stats.OnlyInBCount)
Output:
数组 A 总数: 8
数组 B 总数: 8
数组 A 去重后: 5
数组 B 去重后: 5
重合数量: 2
只在 A 中: 3
只在 B 中: 3

func Compare

func Compare[T comparable](a, b []T, keepOrder ...bool) *CompareResult[T]

Compare 比较两个数组,返回重合部分和不重合部分 参数 keepOrder 表示是否保持原数组顺序

Example

ExampleCompare 基础比较示例

a := []int{1, 2, 3, 4, 5}
b := []int{4, 5, 6, 7, 8}

// 使用 keepOrder=true 保持原数组顺序
result := Compare(a, b, true)

fmt.Printf("数组 A: %v\n", a)
fmt.Printf("数组 B: %v\n", b)
fmt.Printf("重合部分: %v\n", result.Intersection)
fmt.Printf("只在 A 中: %v\n", result.OnlyInA)
fmt.Printf("只在 B 中: %v\n", result.OnlyInB)
Output:
数组 A: [1 2 3 4 5]
数组 B: [4 5 6 7 8]
重合部分: [4 5]
只在 A 中: [1 2 3]
只在 B 中: [6 7 8]
Example (KeepOrder)

ExampleCompare_keepOrder 保持原数组顺序示例

a := []int{5, 4, 3, 2, 1}
b := []int{8, 7, 6, 5, 4}

// 第三个参数 true 表示保持原数组顺序
result := Compare(a, b, true)

fmt.Printf("数组 A: %v\n", a)
fmt.Printf("数组 B: %v\n", b)
fmt.Printf("重合部分(保持A的顺序): %v\n", result.Intersection)
fmt.Printf("只在 A 中(保持A的顺序): %v\n", result.OnlyInA)
fmt.Printf("只在 B 中(保持B的顺序): %v\n", result.OnlyInB)
Output:
数组 A: [5 4 3 2 1]
数组 B: [8 7 6 5 4]
重合部分(保持A的顺序): [5 4]
只在 A 中(保持A的顺序): [3 2 1]
只在 B 中(保持B的顺序): [8 7 6]

func SortAndCompare

func SortAndCompare[T Comparable](a, b []T) *CompareResult[T]

SortAndCompare 排序后比较(适用于有序类型)

type CompareResultFunc

type CompareResultFunc[T any] struct {
	// Intersection 重合部分(两个数组都有的元素,来自数组 A)
	Intersection []T `json:"intersection"`
	// OnlyInA 只在 A 中的元素
	OnlyInA []T `json:"only_in_a"`
	// OnlyInB 只在 B 中的元素
	OnlyInB []T `json:"only_in_b"`
	// Union 并集(所有元素,去重后)
	Union []T `json:"union"`
	// SymmetricDifference 对称差集
	SymmetricDifference []T `json:"symmetric_difference"`
}

CompareResultFunc 使用自定义比较函数的比较结果

func CompareByKey

func CompareByKey[T any, K comparable](a, b []T, keyFn KeyFunc[T, K], keepOrder ...bool) *CompareResultFunc[T]

CompareByKey 使用键提取函数比较两个数组 适用于结构体数组,通过提取关键字段进行比较

Example

ExampleCompareByKey 结构体数组比较示例

type User struct {
	ID   int
	Name string
	Age  int
}

// 两个用户数组,可能有部分用户 ID 相同
usersA := []User{
	{ID: 1, Name: "Alice", Age: 25},
	{ID: 2, Name: "Bob", Age: 30},
	{ID: 3, Name: "Charlie", Age: 35},
}

usersB := []User{
	{ID: 2, Name: "Bob", Age: 30},
	{ID: 3, Name: "Charlie", Age: 35},
	{ID: 4, Name: "David", Age: 40},
}

// 使用 ID 作为比较键
result := CompareByKey(usersA, usersB, func(u User) int { return u.ID })

fmt.Println("基于 ID 的比较结果:")
fmt.Printf("重合用户: %v\n", result.Intersection)
fmt.Printf("只在 A 中的用户: %v\n", result.OnlyInA)
fmt.Printf("只在 B 中的用户: %v\n", result.OnlyInB)

type CompareStats

type CompareStats struct {
	TotalA             int `json:"total_a"`              // A 数组总数
	TotalB             int `json:"total_b"`              // B 数组总数
	IntersectionCount  int `json:"intersection_count"`   // 重合数量
	OnlyInACount       int `json:"only_in_a_count"`      // 只在 A 中的数量
	OnlyInBCount       int `json:"only_in_b_count"`      // 只在 B 中的数量
	UnionCount         int `json:"union_count"`          // 并集数量
	SymmetricDiffCount int `json:"symmetric_diff_count"` // 对称差集数量
	DuplicateCountA    int `json:"duplicate_count_a"`    // A 中重复元素数量
	DuplicateCountB    int `json:"duplicate_count_b"`    // B 中重复元素数量
}

CompareStats 比较统计信息

type KeyFunc

type KeyFunc[T any, K comparable] func(T) K

KeyFunc 键提取函数类型

type Set

type Set[T comparable] struct {
	// contains filtered or unexported fields
}

Set 集合结构体

Example

ExampleSet 集合操作示例

// 创建集合并进行各种操作
s1 := NewSetFromSlice([]int{1, 2, 3, 4, 5})
s2 := NewSetFromSlice([]int{4, 5, 6, 7, 8})

fmt.Printf("集合1: %v\n", s1.ToSlice())
fmt.Printf("集合2: %v\n", s2.ToSlice())
fmt.Printf("交集: %v\n", s1.Intersection(s2).ToSlice())
fmt.Printf("并集: %v\n", s1.Union(s2).ToSlice())
fmt.Printf("差集 (s1-s2): %v\n", s1.Difference(s2).ToSlice())
fmt.Printf("对称差集: %v\n", s1.SymmetricDifference(s2).ToSlice())
fmt.Printf("s1 是否为 s2 的子集: %v\n", s1.IsSubset(s2))

func NewSet

func NewSet[T comparable]() *Set[T]

NewSet 创建新集合

func NewSetFromSlice

func NewSetFromSlice[T comparable](slice []T) *Set[T]

NewSetFromSlice 从切片创建集合

func (*Set[T]) Add

func (s *Set[T]) Add(v T)

Add 添加元素

func (*Set[T]) Clear

func (s *Set[T]) Clear()

Clear 清空集合

func (*Set[T]) Clone

func (s *Set[T]) Clone() *Set[T]

Clone 克隆集合

func (*Set[T]) Contains

func (s *Set[T]) Contains(v T) bool

Contains 判断是否包含元素

func (*Set[T]) Difference

func (s *Set[T]) Difference(other *Set[T]) *Set[T]

Difference 差集(在 s 中但不在 other 中的元素)

func (*Set[T]) Equal

func (s *Set[T]) Equal(other *Set[T]) bool

Equal 判断两个集合是否相等

func (*Set[T]) Intersection

func (s *Set[T]) Intersection(other *Set[T]) *Set[T]

Intersection 交集(重合部分)

func (*Set[T]) IsEmpty

func (s *Set[T]) IsEmpty() bool

IsEmpty 判断是否为空

func (*Set[T]) IsSubset

func (s *Set[T]) IsSubset(other *Set[T]) bool

IsSubset 判断是否为子集

func (*Set[T]) IsSuperset

func (s *Set[T]) IsSuperset(other *Set[T]) bool

IsSuperset 判断是否为超集

func (*Set[T]) Items

func (s *Set[T]) Items() []T

Items 返回集合中所有元素的切片

func (*Set[T]) Range

func (s *Set[T]) Range(f func(T) bool)

Range 遍历集合中的每个元素,执行回调函数 如果回调返回 false,则停止遍历

func (*Set[T]) Remove

func (s *Set[T]) Remove(v T)

Remove 删除元素

func (*Set[T]) Size

func (s *Set[T]) Size() int

Size 返回集合大小

func (*Set[T]) SymmetricDifference

func (s *Set[T]) SymmetricDifference(other *Set[T]) *Set[T]

SymmetricDifference 对称差集(只在其中一个集合中的元素)

func (*Set[T]) ToSlice

func (s *Set[T]) ToSlice() []T

ToSlice 转换为切片

func (*Set[T]) Union

func (s *Set[T]) Union(other *Set[T]) *Set[T]

Union 并集

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL