From 9418238ec3bdaa57038598a08ae8bdffb35647ad Mon Sep 17 00:00:00 2001 From: mikhailsoldatkin Date: Fri, 20 Sep 2024 17:22:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B,=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=BD=D1=87=D0=BC=D0=B0=D1=80=D0=BA=D0=B8,=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 792 ++++++++++++++++++++++++++++++++++++++++ maps/maps.go | 2 +- maps/maps_test.go | 123 +++++++ math/math.go | 1 + math/math_test.go | 33 ++ models/models.go | 18 +- models/models_test.go | 115 +++++- other/other_test.go | 87 +++++ slices/slices_test.go | 125 ++++++- strings/strings_test.go | 35 ++ time/time_test.go | 73 ++++ 12 files changed, 1383 insertions(+), 25 deletions(-) create mode 100644 README.md create mode 100644 maps/maps_test.go create mode 100644 other/other_test.go create mode 100644 strings/strings_test.go create mode 100644 time/time_test.go diff --git a/.gitignore b/.gitignore index 84e1438..3cddf34 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ go.work.sum .env /.idea/* /vendor/* + +# OS related +.DS_Store + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e613b1 --- /dev/null +++ b/README.md @@ -0,0 +1,792 @@ +# go-utils + +`go-utils` — это библиотека утилит для языка программирования Go. Предоставляет удобные обобщённые функции и +инструменты для работы со срезами, строками, временем и многим другим. + +## Пакеты + +- [generics](#generics) +- [maps](#maps) +- [math](#math) +- [models](#models) +- [other](#other) +- [slices](#slices) +- [strings](#strings) +- [time](#time) + +## generics + +Пакет предоставляющий общие типы для использования в других пакетах проекта. Позволяет обобщить функции и +типы данных, чтобы работать с различными типами значений. + +### Основные типы: + +- **[Numeric](#Numeric)** - объединяет все числовые типы, такие как `int`, `float32`, `uint` и другие. + +#### Numeric + +Интерфейс `Numeric` объединяет все числовые типы, такие как `int`, `float32`, `uint` и другие. + +##### Пример использования: + +```go +func Sum[T Numeric](n []T) T { + var sum T + for _, v := range n { + sum += v + } + return sum +} + +``` + +## maps + +Пакет предоставляющий функции для удобной работы с картами (map). + +### Основные функции: + +- **[Has](#Has)**: Проверяет, содержит ли карта данный ключ. +- **[Merge](#Merge)**: Объединяет две карты. Значения из карты "a" имеют более высокий приоритет. +- **[DiffKeys](#DiffKeys)**: Возвращает карту "a" без элементов из карты "b". + +#### Has + +Проверяет, содержит ли карта данный ключ. + +**Параметры:** + +- `m` — карта типа `map[K]V`, где `K` — ключи, а `V` — значения. +- `n` — ключ, который необходимо проверить. + +**Возвращаемое значение:** + +- `bool` — `true`, если ключ присутствует в карте, иначе `false`. + +##### Пример использования: + +```go +m := map[string]int{"one": 1, "two": 2} +exists := maps.Has(m, "two") +// exists: true +``` + +#### Merge + +Объединяет две карты. Значения из карты "a" имеют более высокий приоритет. + +**Параметры:** + +- `a` — первая карта типа `map[K]V`. +- `b` — вторая карта типа `map[K]V`. + +**Возвращаемое значение:** + +- `map[K]V` — новая карта, содержащая объединенные элементы. + +##### Пример использования: + +```go +a := map[string]int{"one": 1, "two": 2} +b := map[string]int{"two": 3, "three": 4} +merged := maps.Merge(a, b) +// merged: {"one": 1, "two": 3, "three": 4} +``` + +#### DiffKeys + +Возвращает карту "a" без элементов из карты "b". + +**Параметры:** + +- `a` — карта типа `map[K]V`, из которой будут удалены элементы. +- `b` — карта типа `map[K]V`, содержащая ключи, которые нужно удалить из карты "a". + +**Возвращаемое значение:** + +- `map[K]V` — новая карта, содержащая элементы из "a", которых нет в "b". + +##### Пример использования: + +```go +a := map[string]int{"one": 1, "two": 2, "three": 3} +b := map[string]int{"two": 2, "four": 4} +diff := maps.DiffKeys(a, b) +// diff: {"one": 1, "three": 3} +``` + +## math + +Пакет предоставляющий математические функции для работы с числовыми типами. + +### Основные функции: + +- **[Max](#Max)**: Возвращает максимальное значение из переданных аргументов. +- **[Min](#Min)**: Возвращает минимальное значение из переданных аргументов. +- **[Sum](#Sum)**: Возвращает сумму всех переданных значений. + +#### Max + +Возвращает максимальное значение из переданных аргументов. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — максимальное значение из переданных аргументов. + +##### Пример использования: + +```go +maxValue := math.Max(1, 2, 3, 4, 5) +// maxValue: 5 +``` + +#### Min + +Возвращает минимальное значение из переданных аргументов. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — минимальное значение из переданных аргументов. + +##### Пример использования: + +```go +minValue := math.Min(1, 2, 3, 4, 5) +// minValue: 1 +``` + +#### Sum + +Возвращает сумму всех переданных значений. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — сумма всех переданных аргументов. + +##### Пример использования: + +```go +total := math.Sum(1, 2, 3, 4, 5) +// total: 15 +``` + +## models + +Пакет предоставляющий функции для работы с сущностями (моделями). + +### Основные функции: + +- **[CollectIDs](#CollectIDs)**: Возвращает срез идентификаторов из среза сущностей. +- **[CollectIDsFromMap](#CollectIDsFromMap)**: Возвращает срез идентификаторов из карты сущностей. +- **[UniqueValues](#UniqueValues)**: Метод для сборки уникальных значений из любого поля модели в срез с желаемым типом + результата. +- **[UniqueValuesFromMap](#UniqueValuesFromMap)**: Метод для сборки уникальных значений из любого поля модели в срез с + желаемым типом результата, используя карту. + +#### CollectIDs + +Возвращает срез идентификаторов из среза сущностей`. + +**Параметры:** + +- `sl` — срез сущностей типа `T`, которые имеют id. + +**Возвращаемое значение:** + +- `[]uint` — срез уникальных идентификаторов. + +##### Пример использования: + +```go +type User struct { + ID uint + // другие поля +} + +func (u User) GetID() uint { + return u.ID +} + +users := []User{{ID: 1}, {ID: 2}, {ID: 1}} +ids := CollectIDs(users) +// ids: []uint{1, 2} +``` + +#### CollectIDsFromMap + +Возвращает срез идентификаторов из карты сущностей, которые имеют id. + +**Параметры:** + +- `m` — карта, где ключи имеют тип `K`, а значения типа `T`, которые имеют id. + +**Возвращаемое значение:** + +- `[]uint` — срез уникальных идентификаторов. + +##### Пример использования: + +```go +type User struct { +ID uint +// другие поля +} + +func (u User) GetID() uint { + return u.ID +} + +userMap := map[string]User{ + "user1": {ID: 1}, + "user2": {ID: 2}, + "user3": {ID: 1}, +} + +ids := CollectIDsFromMap(userMap) +// ids: []uint{1, 2} +``` + +#### UniqueValues + +Метод для сборки уникальных значений из любого поля модели в срез с желаемым типом результата. + +**Параметры:** + +- `slice` — срез сущностей типа `S`. +- `getter` — функция, принимающая сущность типа `S` и возвращающая значение типа `R`, которое будет добавлено в итоговый + срез. + +**Возвращаемое значение:** + +- `[]R` — срез уникальных значений, полученных из переданного среза. + +##### Пример использования: + +```go +type Product struct { +ID uint +Name string +} + +func GetProductID(p Product) uint { + return p.ID +} + +products := []Product{ + {ID: 1, Name: "Product A"}, + {ID: 2, Name: "Product B"}, + {ID: 1, Name: "Product C"}, +} + +uniqueIDs := UniqueValues(products, GetProductID) +// uniqueIDs: []uint{1, 2} +``` + +#### UniqueValuesFromMap + +Метод для сборки уникальных значений из любого поля модели в срез с желаемым типом результата, используя карту. + +**Параметры:** + +- `m` — карта, где ключи имеют тип `K`, а значения имеют тип `V`. +- `getter` — функция, принимающая значение типа `V` и возвращающая значение типа `R`, которое будет добавлено в итоговый + срез. + +**Возвращаемое значение:** + +- `[]R` — срез уникальных значений, полученных из переданной карты. + +##### Пример использования: + +```go +type User struct { + ID uint + Name string +} + +func GetUserName(u User) string { + return u.Name +} + +users := map[string]User{ + "user1": {ID: 1, Name: "Alice"}, + "user2": {ID: 2, Name: "Bob"}, + "user3": {ID: 3, Name: "Alice"}, +} + +uniqueNames := UniqueValuesFromMap(users, GetUserName) +// uniqueNames: []string{"Alice", "Bob"} +``` + +## other + +Пакет, который содержит вспомогательные функции для работы с различными типами данных. + +### Основные функции: + +- **[FirstNonEmpty](#FirstNonEmpty)**: Возвращает первый элемент с ненулевым значением из переданных аргументов. + +#### FirstNonEmpty + +Возвращает первый элемент с ненулевым значением из переданных аргументов. + +**Параметры:** + +- `tt` — переменное количество аргументов типа `T`, где `T` — любой тип, поддерживающий сравнение. + +**Возвращаемое значение:** + +- `T` — первый ненулевой элемент из переданных аргументов. Если все элементы равны нулю, возвращает значение по + умолчанию для типа `T`. + +##### Пример использования: + +```go +first := other.FirstNonEmpty("", "hello", "world") +// first: "hello" + +firstNum := other.FirstNonEmpty(0, 1, 2) +// firstNum: 1 + +firstNil := other.FirstNonEmpty(nil, nil) +// firstNil: nil +``` + +## slices + +Пакет предоставляющий функции для работы со срезами (slice). + +### Основные функции: + +- **[ConvertSlice](#ConvertSlice)**: изменяет тип элементов в срезе. +- **[FilterNil](#FilterNil)**: возвращает срез без пустых значений (например, 0, "", и т.д.), модифицируя оригинальный + срез. +- **[Unique](#Unique)**: возвращает срез без дубликатов, модифицируя оригинальный срез. +- **[Union](#Union)**: объединяет два среза, исключая дубликаты. +- **[Cross](#Cross)**: возвращает срез со значениями, присутствующими в обоих срезах. +- **[IsEqual](#IsEqual)**: проверяет, идентичны ли срезы, независимо от порядка элементов. +- **[Has](#Has)**: проверяет, содержит ли срез данное значение. +- **[TrimStrings](#TrimStrings)**: удаляет пробелы из каждого элемента строкового среза. +- **[ToKeyMap](#ToKeyMap)**: возвращает карту с ключами, равными значениям среза. +- **[SliceDiff](#SliceDiff)**: возвращает срез, содержащий элементы, присутствующие в первом срезе, но отсутствующие в + остальных. +- **[SliceIntersect](#SliceIntersect)**: возвращает срез с уникальными значениями, присутствующими во всех переданных + срезах. +- **[Max](#Max)**: возвращает максимальное значение из представленных элементов. +- **[Min](#Min)**: возвращает минимальное значение из представленных элементов. +- **[Sum](#Sum)**: возвращает сумму всех значений. + +### ConvertSlice + +Функция, которая изменяет тип элементов в срезе. + +**Параметры:** + +- `s` — срез элементов типа `T`, который необходимо преобразовать. + +**Возвращаемое значение:** + +- `[]R` — новый срез элементов типа `R`, полученный путем преобразования элементов из среза `s`. + +**Пример использования:** + +```go +newSlice := slices.ConvertSlice[int32, uint]([]int32{1, 2, 3}) +// newSlice: []uint{1, 2, 3} +``` + +### FilterNil + +Функция, которая возвращает срез без пустых значений (например, `0`, `""` и т. д.). Обратите внимание, что +она модифицирует оригинальный срез. + +**Параметры:** + +- `sl` — срез элементов типа `T`, из которого будут удалены пустые значения. + +**Возвращаемое значение:** + +- `[]T` — срез, содержащий только непустые значения. + +**Пример использования:** + +```go +values := []int{0, 1, 2, 0, 3} +filtered := slices.FilterNil(values) +// filtered: []int{1, 2, 3} +``` + +### Unique + +Функция, которая возвращает срез без дубликатов. Обратите внимание, что она модифицирует оригинальный срез. + +**Параметры:** + +- `sl` — срез элементов типа `T`, из которого будут удалены дубликаты. + +**Возвращаемое значение:** + +- `[]T` — срез, содержащий только уникальные значения. + +**Пример использования:** + +```go +values := []int{1, 2, 2, 3, 4, 4} +uniqueValues := slices.Unique(values) +// uniqueValues: []int{1, 2, 3, 4} +``` + +### Union + +Функция, которая объединяет два среза, исключая дубликаты. + +**Параметры:** + +- `sl1` — первый срез типа `T`. +- `sl2` — второй срез типа `T`. + +**Возвращаемое значение:** + +- `[]T` — новый срез, содержащий уникальные значения из обоих входных срезов. + +**Пример использования:** + +```go +slice1 := []int{1, 2, 3} +slice2 := []int{3, 4, 5} +result := slices.Union(slice1, slice2) +// result: []int{1, 2, 3, 4, 5} +``` + +### Cross + +Функция, которая возвращает срез значений, присутствующих в обоих входных срезах. + +**Параметры:** + +- `sl1` — первый срез типа `T`. +- `sl2` — второй срез типа `T`. + +**Возвращаемое значение:** + +- `[]T` — новый срез, содержащий значения, которые присутствуют в обоих входных срезах. + +**Пример использования:** + +```go +slice1 := []int{1, 2, 3} +slice2 := []int{2, 3, 4} +result := slices.Cross(slice1, slice2) +// result: []int{2, 3} +``` + +### IsEqual + +Функция, которая проверяет, идентичны ли два среза, независимо от порядка элементов. + +**Параметры:** + +- `sl1` — первый срез типа `T`. +- `sl2` — второй срез типа `T`. + +**Возвращаемое значение:** + +- `bool` — `true`, если срезы идентичны (содержат одинаковые элементы в любом порядке), иначе `false`. + +**Пример использования:** + +```go +slice1 := []int{1, 2, 3} +slice2 := []int{3, 2, 1} +isEqual := slices.IsEqual(slice1, slice2) +// isEqual: true +``` + +### Has + +Функция, которая проверяет, содержит ли срез заданное значение. + +**Параметры:** + +- `sl` — срез типа `T`, в котором выполняется поиск. +- `n` — значение типа `T`, которое необходимо найти в срезе. + +**Возвращаемое значение:** + +- `bool` — `true`, если значение присутствует в срезе, иначе `false`. + +**Пример использования:** + +```go +slice := []string{"apple", "banana", "cherry"} +exists := slices.Has(slice, "banana") +// exists: true +``` + +### TrimStrings + +Функция, которая обрезает пробелы в начале и конце каждой строки в срезе строк. + +**Параметры:** + +- `ss` — срез строк, который нужно обработать. + +**Возвращаемое значение:** + +- `[]string` — срез строк, где каждая строка обрезана от пробелов. + +**Пример использования:** + +```go +strings := []string{" apple ", " banana ", "cherry "} +trimmed := slices.TrimStrings(strings) +// trimmed: []string{"apple", "banana", "cherry"} +``` + +### ToKeyMap + +Функция, которая преобразует срез значений в карту, где ключами являются элементы среза, а значениями — +булевый тип, указывающий на наличие этих ключей. + +**Параметры:** + +- `sl` — срез значений типа `T`, который будет преобразован в карту. + +**Возвращаемое значение:** + +- `map[T]bool` — карта, где ключами являются элементы из среза, а значениями — `true`. + +**Пример использования:** + +```go +values := []string{"apple", "banana", "cherry"} +keyMap := slices.ToKeyMap(values) +// keyMap: map[string]bool{"apple": true, "banana": true, "cherry": true} +``` + +### SliceDiff + +Функция, которая возвращает срез, содержащий элементы, присутствующие в первом срезе, но отсутствующие в +других переданных срезах. + +**Параметры:** + +- `slices` — переменное количество срезов типа `T`, из которых будет вычислено различие. + +**Возвращаемое значение:** + +- `[]T` — срез, содержащий элементы из первого среза, которых нет в остальных. + +**Пример использования:** + +```go +slice1 := []int{1, 2, 3, 4} +slice2 := []int{3, 4, 5} +slice3 := []int{4, 5, 6} + +result := slices.SliceDiff(slice1, slice2, slice3) +// result: []int{1, 2} +``` + +### SliceIntersect + +Функция, которая возвращает срез с уникальными значениями, присутствующими в обоих (или всех) +переданных срезах. + +**Параметры:** + +- `slices` — переменное количество срезов типа `T`, из которых будет вычислено пересечение. + +**Возвращаемое значение:** + +- `[]T` — срез, содержащий уникальные элементы, которые присутствуют во всех переданных срезах. + +**Пример использования:** + +```go +slice1 := []int{1, 2, 3, 4} +slice2 := []int{3, 4, 5} +slice3 := []int{4, 5, 6} + +result := slices.SliceIntersect(slice1, slice2, slice3) +// result: []int{4} +``` + +### Max + +Функция, которая возвращает максимальное значение из переданных аргументов. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — максимальное значение из переданных аргументов. + +**Пример использования:** + +```go +maxValue := slices.Max([]int{1, 2, 3, 4, 5}) +// maxValue: 5 +``` + +### Min + +Функция, которая возвращает минимальное значение из переданных аргументов. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — минимальное значение из переданных аргументов. + +**Пример использования:** + +```go +minValue := slices.Min([]int{1, 2, 3, 4, 5}) +// minValue: 1 +``` + +### Sum + +Функция, которая возвращает сумму всех переданных значений. + +**Параметры:** + +- `n` — переменное количество аргументов типа `T`, где `T` — любой числовой тип, поддерживаемый интерфейсом `Numeric`. + +**Возвращаемое значение:** + +- `T` — сумма всех переданных аргументов. + +**Пример использования:** + +```go +total := slices.Sum([]int{1, 2, 3, 4, 5}) +// total: 15 +``` + +# strings + +Пакет, предоставляющий функции для работы со строками. + +### Основные функции: + +- **[Truncate](#Truncate)**: Усечет строку до заданного количества рун. + +#### Truncate + +Усечет строку до заданного количества рун. + +**Параметры:** + +- `str` — строка, которую необходимо усечь. +- `maxRunes` — максимальное количество рун, до которого строка будет усечена. + +**Возвращаемое значение:** + +- `string` — усеченная строка, если длина превышает `maxRunes`, иначе оригинальная строка. + +##### Пример использования: + +```go +result := strings.Truncate("Hello, World!", 5) +// result: "Hello" +``` + +## time + +Пакет, предоставляющий функции для работы с временными значениями. + +### Основные функции: + +- **[Midnight](#Midnight)**: Возвращает время, соответствующее полуночи для текущей даты в локальном часовом поясе. +- **[MidnightByLocation](#MidnightByLocation)**: Возвращает время полуночи для указанного часового пояса. +- **[MidnightByTimeZone](#MidnightByTimeZone)**: Возвращает время полуночи для указанного часового пояса. + +#### Midnight + +Возвращает время, соответствующее полуночи для текущей даты в локальном часовом поясе. + +**Возвращаемое значение:** + +- `time.Time` — значение времени, соответствующее полуночи. +- `error` — ошибка, если произошла проблема с вычислением времени (обычно не возникает). + +##### Пример использования: + +```go +midnight, err := time.Midnight() +if err != nil { +// Обработка ошибки +} +// midnight: 2024-09-20 00:00:00 +0000 UTC +``` + +#### MidnightByLocation + +Возвращает время полуночи для указанного часового пояса. + +**Параметры:** + +- `loc` — указатель на структуру `time.Location`, представляющую часовой пояс. + +**Возвращаемое значение:** + +- `time.Time` — значение времени, соответствующее полуночи в указанном часовом поясе. +- `error` — ошибка, если произошла проблема с вычислением времени (обычно не возникает). + +##### Пример использования: + +```go +loc, err := time.LoadLocation("Europe/Moscow") +if err != nil { + // Обработка ошибки +} + +midnight, err := time.MidnightByLocation(loc) +if err != nil { + // Обработка ошибки +} +// midnight: 2024-09-20 00:00:00 +0300 MSK +``` + +#### MidnightByTimeZone + +Возвращает время полуночи для указанного часового пояса. + +**Параметры:** + +- `timeZone` — строка, представляющая название часового пояса (например, "Europe/Moscow"). + +**Возвращаемое значение:** + +- `time.Time` — значение времени, соответствующее полуночи в указанном часовом поясе. +- `error` — ошибка, если произошла проблема с загрузкой часового пояса (например, если указанный часовой пояс не + существует). + +##### Пример использования: + +```go +midnight, err := time.MidnightByTimeZone("Europe/Moscow") +if err != nil { + // Обработка ошибки +} +// midnight: 2024-09-20 00:00:00 +0300 MSK +``` + diff --git a/maps/maps.go b/maps/maps.go index dc0f16a..4e6e270 100644 --- a/maps/maps.go +++ b/maps/maps.go @@ -1,6 +1,6 @@ package maps -// Has проверяет, содержит ли карта данное значение. +// Has checks if the map contains the given key. func Has[K comparable, V any](m map[K]V, n K) bool { _, ok := m[n] diff --git a/maps/maps_test.go b/maps/maps_test.go new file mode 100644 index 0000000..f402f3c --- /dev/null +++ b/maps/maps_test.go @@ -0,0 +1,123 @@ +package maps + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHas(t *testing.T) { + // Проверка существующего ключа + m := map[int]string{1: "one", 2: "two", 3: "three"} + assert.True(t, Has(m, 1)) + assert.True(t, Has(m, 2)) + + // Проверка отсутствующего ключа + assert.False(t, Has(m, 4)) + + // Проверка пустой карты + emptyMap := make(map[int]string) + assert.False(t, Has(emptyMap, 1)) +} + +func TestMerge(t *testing.T) { + // Две непересекающиеся карты + a := map[int]string{1: "one", 2: "two"} + b := map[int]string{3: "three", 4: "four"} + expected := map[int]string{1: "one", 2: "two", 3: "three", 4: "four"} + result := Merge(a, b) + assert.Equal(t, expected, result) + + // Пересекающиеся ключи, значения карты a должны быть приоритетными + a = map[int]string{1: "uno", 2: "dos"} + b = map[int]string{1: "one", 3: "three"} + expected = map[int]string{1: "uno", 2: "dos", 3: "three"} + result = Merge(a, b) + assert.Equal(t, expected, result) + + // Пустая карта b + a = map[int]string{1: "one", 2: "two"} + b = map[int]string{} + expected = map[int]string{1: "one", 2: "two"} + result = Merge(a, b) + assert.Equal(t, expected, result) + + // Пустая карта a + a = map[int]string{} + b = map[int]string{3: "three", 4: "four"} + expected = map[int]string{3: "three", 4: "four"} + result = Merge(a, b) + assert.Equal(t, expected, result) +} + +func TestDiffKeys(t *testing.T) { + // a содержит ключи, которых нет в b + a := map[int]string{1: "one", 2: "two", 3: "three"} + b := map[int]string{2: "two", 4: "four"} + expected := map[int]string{1: "one", 3: "three"} + result := DiffKeys(a, b) + assert.Equal(t, expected, result) + + // Все ключи из a есть в b + a = map[int]string{1: "one", 2: "two"} + b = map[int]string{1: "one", 2: "two"} + expected = map[int]string{} + result = DiffKeys(a, b) + assert.Equal(t, expected, result) + + // Пустая карта b + a = map[int]string{1: "one", 2: "two"} + b = map[int]string{} + expected = map[int]string{1: "one", 2: "two"} + result = DiffKeys(a, b) + assert.Equal(t, expected, result) + + // Пустая карта a + a = map[int]string{} + b = map[int]string{1: "one", 2: "two"} + expected = map[int]string{} + result = DiffKeys(a, b) + assert.Equal(t, expected, result) +} + +func BenchmarkHas(b *testing.B) { + m := make(map[int]string, 1000) + for i := 0; i < 1000; i++ { + m[i] = "value" + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Has(m, i%1000) + } +} + +func BenchmarkMerge(b *testing.B) { + a := make(map[int]string, 1000) + bm := make(map[int]string, 1000) + for i := 0; i < 1000; i++ { + a[i] = "a_value" + bm[i] = "b_value" + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Merge(a, bm) + } +} + +func BenchmarkDiffKeys(b *testing.B) { + a := make(map[int]string, 1000) + bm := make(map[int]string, 500) + for i := 0; i < 1000; i++ { + a[i] = "value" + if i < 500 { + bm[i] = "value" + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + DiffKeys(a, bm) + } +} diff --git a/math/math.go b/math/math.go index a25eab4..a09d276 100644 --- a/math/math.go +++ b/math/math.go @@ -6,6 +6,7 @@ import ( // Max return maximum value from presented func Max[T generics.Numeric](n ...T) T { + // TODO нужна ли проверка на пустоту передаваемого среза? m := n[0] for i := 1; i < len(n); i++ { if n[i] > m { diff --git a/math/math_test.go b/math/math_test.go index a57af4a..9652a4c 100644 --- a/math/math_test.go +++ b/math/math_test.go @@ -44,3 +44,36 @@ func TestSum(t *testing.T) { var expInt8 int8 = 52 assert.Equal(t, expInt8, sumInt8) } + +func BenchmarkMax(b *testing.B) { + ints := []int{-2, 3, 15, 28, 4, 100, 99, 42} + floats := []float64{-2.2, 3.2, 15.5, 28.1, 4.4, 100.8, 99.9, 42.2} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Max(ints...) + Max(floats...) + } +} + +func BenchmarkMin(b *testing.B) { + ints := []int{1, -2, 3, 15, 28, 4, -100, 42} + floats := []float64{1.1, -2.2, 3.2, 15.8, 28.1, 4.4, -100.7, 42.3} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Min(ints...) + Min(floats...) + } +} + +func BenchmarkSum(b *testing.B) { + ints := []int{1, -2, 3, 15, 28, 4, 100, 42, 10, 99} + floats := []float64{1.1, -2.2, 3.2, 15.8, 28.1, 4.4, 100.7, 42.3, 99.9} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Sum(ints...) + Sum(floats...) + } +} diff --git a/models/models.go b/models/models.go index a1e5699..1e8629a 100644 --- a/models/models.go +++ b/models/models.go @@ -8,32 +8,32 @@ type HasID interface { GetID() uint } -// CollectIDs возвращает слайс ид из слайса сущностей, имеющих ид. +// CollectIDs returns a slice of IDs from a slice of entities that have ID. func CollectIDs[T HasID](sl []T) []uint { return UniqueValues(sl, T.GetID) } -// CollectIDsFromMap возвращает слайс ид из слайса сущностей, имеющих ид. +// CollectIDsFromMap returns a slice of IDs from a map of entities that have ID. func CollectIDsFromMap[K comparable, T HasID](m map[K]T) []uint { return UniqueValuesFromMap(m, T.GetID) } // UniqueValues a method for assembling unique values of any model field into a slice with the desired result type. -func UniqueValues[S interface{}, R comparable](slice []S, getter func(S) R) []R { - ids := make([]R, 0, len(slice)) +func UniqueValues[S any, R comparable](slice []S, getter func(S) R) []R { + values := make([]R, 0, len(slice)) for _, v := range slice { - ids = append(ids, getter(v)) + values = append(values, getter(v)) } - return slices.Unique(ids) + return slices.Unique(values) } // UniqueValuesFromMap a method for assembling unique values of any model field into a slice with the desired result type. func UniqueValuesFromMap[K comparable, V any, R comparable](m map[K]V, getter func(V) R) []R { - ids := make([]R, 0, len(m)) + values := make([]R, 0, len(m)) for _, v := range m { - ids = append(ids, getter(v)) + values = append(values, getter(v)) } - return slices.Unique(ids) + return slices.Unique(values) } diff --git a/models/models_test.go b/models/models_test.go index 7811d29..2f93e24 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -1,31 +1,128 @@ package models import ( + "fmt" + "strconv" "testing" "github.com/stretchr/testify/assert" ) -type testModel struct { - id uint +type testModel[T comparable] struct { + id uint + property T } -func (m *testModel) GetID() uint { +func (m *testModel[T]) GetID() uint { return m.id } +func (m *testModel[T]) GetProperty() T { + return m.property +} + func TestCollectIDs(t *testing.T) { - sl := []*testModel{{id: 3}, {id: 5}, {id: 1}} + sl := []*testModel[string]{{id: 3}, {id: 5}, {id: 1}} ids := CollectIDs(sl) - exp := []uint{3, 5, 1} - assert.Equal(t, exp, ids) + expected := []uint{3, 5, 1} + assert.ElementsMatch(t, expected, ids) } func TestCollectIDsFromMap(t *testing.T) { - m := map[string]*testModel{"first": {id: 3}, "second": {id: 5}, "third": {id: 1}} + m := map[string]*testModel[string]{ + "instance1": {id: 3}, + "instance2": {id: 5}, + "instance3": {id: 1}, + } ids := CollectIDsFromMap(m) - exp := []uint{3, 5, 1} - assert.Equal(t, exp, ids) + expected := []uint{3, 5, 1} + assert.ElementsMatch(t, expected, ids) +} + +func TestUniqueValues(t *testing.T) { + sl := []*testModel[string]{ + {id: 1, property: "value1"}, + {id: 2, property: "value2"}, + {id: 3, property: "value1"}, // Дубликат + } + + properties := UniqueValues(sl, func(m *testModel[string]) string { + return m.GetProperty() + }) + expected := []string{"value1", "value2"} + assert.ElementsMatch(t, expected, properties) +} + +func TestUniqueValuesFromMap(t *testing.T) { + m := map[string]*testModel[string]{ + "instance1": {id: 1, property: "value1"}, + "instance2": {id: 2, property: "value2"}, + "instance3": {id: 3, property: "value1"}, // Дубликат + } + + properties := UniqueValuesFromMap(m, func(m *testModel[string]) string { + return m.GetProperty() + }) + expected := []string{"value1", "value2"} + assert.ElementsMatch(t, expected, properties) +} + +func BenchmarkCollectIDs(b *testing.B) { + sl := make([]*testModel[struct{}], 0, 1000) + for i := 0; i < 1000; i++ { + sl = append(sl, &testModel[struct{}]{id: uint(i)}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + CollectIDs(sl) + } +} + +func BenchmarkCollectIDsFromMap(b *testing.B) { + m := make(map[string]*testModel[struct{}], 1000) + for i := 0; i < 1000; i++ { + m[strconv.Itoa(i)] = &testModel[struct{}]{id: uint(i)} + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + CollectIDsFromMap(m) + } +} + +func BenchmarkUniqueValues(b *testing.B) { + sl := make([]*testModel[string], 1000) + for i := 0; i < 1000; i++ { + sl[i] = &testModel[string]{id: uint(i), property: fmt.Sprintf("value%d", i)} + if i%10 == 0 { + sl[i].property = "duplicateValue" // Создание дубликата + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + UniqueValues(sl, func(m *testModel[string]) string { + return m.GetProperty() + }) + } +} + +func BenchmarkUniqueValuesFromMap(b *testing.B) { + m := make(map[string]*testModel[string], 1000) + for i := 0; i < 1000; i++ { + m[strconv.Itoa(i)] = &testModel[string]{id: uint(i), property: fmt.Sprintf("value%d", i)} + if i%10 == 0 { + m[strconv.Itoa(i)].property = "duplicateValue" // Создание дубликата + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + UniqueValuesFromMap(m, func(m *testModel[string]) string { + return m.GetProperty() + }) + } } diff --git a/other/other_test.go b/other/other_test.go new file mode 100644 index 0000000..7f509a7 --- /dev/null +++ b/other/other_test.go @@ -0,0 +1,87 @@ +package other + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFirstNonEmpty(t *testing.T) { + stringCases := []struct { + name string + input []string + expected string + }{ + {"AllEmpty", []string{"", "", ""}, ""}, + {"FirstNonEmpty", []string{"", "value", "another"}, "value"}, + {"OnlyNonEmpty", []string{"value"}, "value"}, + {"AllNonEmpty", []string{"value1", "value2"}, "value1"}, + {"MixedZeroAndValue", []string{"0", "1", "0"}, "0"}, + } + + for _, tt := range stringCases { + t.Run(tt.name, func(t *testing.T) { + result := FirstNonEmpty(tt.input...) + assert.Equal(t, tt.expected, result) + }) + } + + intCases := []struct { + name string + input []int + expected int + }{ + {"AllEmpty", []int{0, 0, 0}, 0}, + {"FirstNonEmpty", []int{0, 1, 2}, 1}, + {"OnlyNonEmpty", []int{1}, 1}, + {"AllNonEmpty", []int{1, 2}, 1}, + {"MixedZeroAndValue", []int{0, 1, 0}, 1}, + } + + for _, tt := range intCases { + t.Run(tt.name, func(t *testing.T) { + result := FirstNonEmpty(tt.input...) + assert.Equal(t, tt.expected, result) + }) + } +} + +func BenchmarkFirstNonEmpty(b *testing.B) { + stringCases := []struct { + name string + args []string + }{ + {"AllEmpty", []string{"", "", ""}}, + {"FirstNonEmpty", []string{"", "value", "another"}}, + {"OnlyNonEmpty", []string{"value"}}, + {"AllNonEmpty", []string{"value1", "value2"}}, + {"MixedZeroAndValue", []string{"0", "1", "0"}}, + } + + for _, tt := range stringCases { + b.Run(tt.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + FirstNonEmpty(tt.args...) + } + }) + } + + intCases := []struct { + name string + args []int + }{ + {"AllEmpty", []int{0, 0, 0}}, + {"FirstNonEmpty", []int{0, 1, 2}}, + {"OnlyNonEmpty", []int{1}}, + {"AllNonEmpty", []int{1, 2}}, + {"MixedZeroAndValue", []int{0, 1, 0}}, + } + + for _, tt := range intCases { + b.Run(tt.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + FirstNonEmpty(tt.args...) + } + }) + } +} diff --git a/slices/slices_test.go b/slices/slices_test.go index 7654265..c133ff7 100644 --- a/slices/slices_test.go +++ b/slices/slices_test.go @@ -18,14 +18,53 @@ func TestUnique(t *testing.T) { assert.Equal(t, []uint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, r) } +func TestUnion(t *testing.T) { + r := Union([]uint{1, 2, 3, 4}, []uint{3, 4, 5}) + assert.ElementsMatch(t, []uint{1, 2, 3, 4, 5}, r) +} + +func TestCross(t *testing.T) { + r := Cross([]uint{1, 2, 3}, []uint{2, 3, 4}) + assert.Equal(t, []uint{2, 3}, r) +} + +func TestIsEqual(t *testing.T) { + assert.True(t, IsEqual([]uint{1, 2, 3}, []uint{3, 2, 1})) + assert.False(t, IsEqual([]uint{1, 2}, []uint{1, 2, 3})) +} + +func TestHas(t *testing.T) { + assert.True(t, Has([]uint{1, 2, 3}, 2)) + assert.False(t, Has([]uint{1, 2, 3}, 4)) +} + +func TestTrimStrings(t *testing.T) { + r := TrimStrings([]string{" hello ", " world ", " ! "}) + assert.Equal(t, []string{"hello", "world", "!"}, r) +} + +func TestToKeyMap(t *testing.T) { + r := ToKeyMap([]string{"a", "b", "a", "c"}) + expected := map[string]bool{"a": true, "b": true, "c": true} + assert.Equal(t, expected, r) +} + +func TestSliceDiff(t *testing.T) { + r := SliceDiff([]uint{1, 2, 3, 4}, []uint{2, 3}) + assert.Equal(t, []uint{1, 4}, r) +} + +func TestSliceIntersect(t *testing.T) { + r := SliceIntersect([]uint{1, 2, 3}, []uint{2, 3}, []uint{3, 4}) + assert.Equal(t, []uint{3}, r) +} + func TestMax(t *testing.T) { assert.Equal(t, 28, Max([]int{-2, 3, 15, 28, 4})) assert.Equal(t, 28.1, Max([]float64{-2.2, 3.2, 15, 28.1, 4.4})) - uintMax := Max([]uint{2, 3, 15, 28, 4}) var uintExp uint = 28 assert.Equal(t, uintExp, uintMax) - int8Val := Max([]int8{-2, 3, 15, 28, 4}) var expInt8 int8 = 28 assert.Equal(t, expInt8, int8Val) @@ -34,11 +73,9 @@ func TestMax(t *testing.T) { func TestMin(t *testing.T) { assert.Equal(t, -2, Min([]int{1, -2, 3, 15, 28, 4})) assert.Equal(t, -2.2, Min([]float64{1.1, -2.2, 3.2, 15, 28.1, 4.4})) - minUint := Min([]uint{4, 2, 3, 15, 28, 4}) var expUint uint = 2 assert.Equal(t, expUint, minUint) - minInt8 := Min([]int8{4, -2, 3, 15, 28, 4}) var expInt8 int8 = -2 assert.Equal(t, expInt8, minInt8) @@ -47,12 +84,88 @@ func TestMin(t *testing.T) { func TestSum(t *testing.T) { assert.Equal(t, 49, Sum([]int{1, -2, 3, 15, 28, 4})) assert.Equal(t, 49.6, Sum([]float64{1.1, -2.2, 3.2, 15, 28.1, 4.4})) - sumUint := Sum([]uint{4, 2, 3, 15, 28, 4}) var uintExp uint = 56 assert.Equal(t, uintExp, sumUint) - sumInt8 := Sum([]int8{4, -2, 3, 15, 28, 4}) var expInt8 int8 = 52 assert.Equal(t, expInt8, sumInt8) } + +func BenchmarkFilterNil(b *testing.B) { + for i := 0; i < b.N; i++ { + FilterNil([]uint{0, 0, 1, 2, 0, 3, 4, 0, 5}) + } +} + +func BenchmarkUnique(b *testing.B) { + for i := 0; i < b.N; i++ { + Unique([]uint{0, 1, 2, 2, 3, 1, 4, 5, 0, 6}) + } +} + +func BenchmarkUnion(b *testing.B) { + for i := 0; i < b.N; i++ { + Union([]uint{1, 2, 3}, []uint{3, 4, 5}) + } +} + +func BenchmarkCross(b *testing.B) { + for i := 0; i < b.N; i++ { + Cross([]uint{1, 2, 3}, []uint{2, 3, 4}) + } +} + +func BenchmarkIsEqual(b *testing.B) { + for i := 0; i < b.N; i++ { + IsEqual([]uint{1, 2, 3}, []uint{3, 2, 1}) + } +} + +func BenchmarkHas(b *testing.B) { + for i := 0; i < b.N; i++ { + Has([]uint{1, 2, 3}, 2) + } +} + +func BenchmarkTrimStrings(b *testing.B) { + for i := 0; i < b.N; i++ { + TrimStrings([]string{" hello ", " world ", " ! "}) + } +} + +func BenchmarkToKeyMap(b *testing.B) { + for i := 0; i < b.N; i++ { + ToKeyMap([]string{"a", "b", "a", "c"}) + } +} + +func BenchmarkSliceDiff(b *testing.B) { + for i := 0; i < b.N; i++ { + SliceDiff([]uint{1, 2, 3, 4}, []uint{2, 3}) + } +} + +func BenchmarkSliceIntersect(b *testing.B) { + for i := 0; i < b.N; i++ { + SliceIntersect([]uint{1, 2, 3}, []uint{2, 3}, []uint{3, 4}) + } +} + +func BenchmarkMax(b *testing.B) { + for i := 0; i < b.N; i++ { + Max([]int{-2, 3, 15, 28, 4}) + } +} + +func BenchmarkMin(b *testing.B) { + for i := 0; i < b.N; i++ { + Min([]int{1, -2, 3, 15, 28, 4}) + } +} + +func BenchmarkSum(b *testing.B) { + for i := 0; i < b.N; i++ { + Sum([]int{1, -2, 3, 15, 28, 4}) + } +} diff --git a/strings/strings_test.go b/strings/strings_test.go new file mode 100644 index 0000000..da4b7d5 --- /dev/null +++ b/strings/strings_test.go @@ -0,0 +1,35 @@ +package strings + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTruncate(t *testing.T) { + tests := []struct { + name string + input string + maxRunes uint + expected string + }{ + {"EmptyString", "", 10, ""}, + {"ShortString", "abc", 5, "abc"}, + {"ExactLength", "abcdef", 6, "abcdef"}, + {"Truncated", "abcdef", 4, "abcd"}, + {"LongString", "Привет мир", 6, "Привет"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Truncate(tt.input, tt.maxRunes) + assert.Equal(t, tt.expected, result) + }) + } +} + +func BenchmarkTruncate(b *testing.B) { + for i := 0; i < b.N; i++ { + Truncate("This is a long string for benchmark testing", 10) + } +} diff --git a/time/time_test.go b/time/time_test.go new file mode 100644 index 0000000..a9eb732 --- /dev/null +++ b/time/time_test.go @@ -0,0 +1,73 @@ +package time + +import ( + "log" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMidnight(t *testing.T) { + midnight, err := Midnight() + assert.NoError(t, err) + + assert.Equal(t, 0, midnight.Hour()) + assert.Equal(t, 0, midnight.Minute()) + assert.Equal(t, 0, midnight.Second()) + assert.Equal(t, 0, midnight.Nanosecond()) +} + +func TestMidnightByLocation(t *testing.T) { + loc, _ := time.LoadLocation("Europe/Moscow") + midnight, err := MidnightByLocation(loc) + assert.NoError(t, err) + + assert.Equal(t, 0, midnight.Hour()) + assert.Equal(t, 0, midnight.Minute()) + assert.Equal(t, 0, midnight.Second()) + assert.Equal(t, 0, midnight.Nanosecond()) + assert.Equal(t, loc, midnight.Location()) +} + +func TestMidnightByTimeZone(t *testing.T) { + midnight, err := MidnightByTimeZone("Europe/Moscow") + assert.NoError(t, err) + + assert.Equal(t, 0, midnight.Hour()) + assert.Equal(t, 0, midnight.Minute()) + assert.Equal(t, 0, midnight.Second()) + assert.Equal(t, 0, midnight.Nanosecond()) + assert.Equal(t, "Europe/Moscow", midnight.Location().String()) + + _, err = MidnightByTimeZone("Invalid/Timezone") + assert.Error(t, err) +} + +func BenchmarkMidnight(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := Midnight() + if err != nil { + log.Fatal(err) + } + } +} + +func BenchmarkMidnightByLocation(b *testing.B) { + loc, _ := time.LoadLocation("America/New_York") + for i := 0; i < b.N; i++ { + _, err := MidnightByLocation(loc) + if err != nil { + log.Fatal(err) + } + } +} + +func BenchmarkMidnightByTimeZone(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := MidnightByTimeZone("Asia/Tokyo") + if err != nil { + log.Fatal(err) + } + } +}