allow changing window size

pull/372/head
Jesse Duffield 2 years ago
parent df1a0b7c07
commit badc43b225

@ -16,13 +16,15 @@ require (
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
github.com/mattn/go-runewidth v0.0.13
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
github.com/mgutz/str v1.2.0
github.com/samber/lo v1.20.0
github.com/samber/lo v1.31.0
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.8.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
)
@ -39,7 +41,6 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
@ -57,6 +58,6 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.2.0 // indirect
)

@ -54,6 +54,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA=
github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg=
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@ -99,8 +101,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/samber/lo v1.20.0 h1:20FtphdORvp4yxklurzZv2HX+g+0urEMQziODC5bV70=
github.com/samber/lo v1.20.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@ -108,11 +110,14 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
@ -177,8 +182,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=

@ -114,6 +114,16 @@ type GuiConfig struct {
// By default, containers are now sorted by status. This setting allows users to
// use legacy behaviour instead.
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
// If 0.333, then the side panels will be 1/3 of the screen's width
SidePanelWidth float64 `yaml:"sidePanelWidth"`
// Determines whether we show the bottom line (the one containing keybinding
// info and the status of the app).
ShowBottomLine bool `yaml:"showBottomLine"`
// When true, ensures that the focused side panel always takes up the full vertical space
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
}
// CommandTemplatesConfig determines what commands actually get called when we
@ -320,10 +330,13 @@ func GetDefaultConfig() UserConfig {
InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"},
},
ShowAllContainers: false,
ReturnImmediately: false,
WrapMainPanel: true,
LegacySortContainers: false,
ShowAllContainers: false,
ReturnImmediately: false,
WrapMainPanel: true,
LegacySortContainers: false,
SidePanelWidth: 0.3333,
ShowBottomLine: true,
ExpandFocusedSidePanel: false,
},
ConfirmOnQuit: false,
Logs: LogsConfig{

@ -0,0 +1,237 @@
package gui
import (
"github.com/jesseduffield/lazycore/pkg/boxlayout"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mattn/go-runewidth"
"github.com/samber/lo"
)
// In this file we use the boxlayout package, along with knowledge about the app's state,
// to arrange the windows (i.e. panels) on the screen.
const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
minimumHeight := 9
minimumWidth := 10
width, height := gui.g.Size()
if width < minimumWidth || height < minimumHeight {
return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
}
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Window: "main",
Weight: mainSectionWeight,
},
},
},
{
Direction: boxlayout.COLUMN,
Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
result := map[K]V{}
for _, currMap := range maps {
for key, value := range currMap {
result[key] = value
}
}
return result
}
func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 1
sideSectionWeight = 0
} else {
if gui.State.ScreenMode == SCREEN_HALF {
mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 0
}
}
return sideSectionWeight, mainSectionWeight
}
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
result := []*boxlayout.Box{}
if len(appStatus) > 0 {
result = append(result,
&boxlayout.Box{
Window: "appStatus",
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
},
)
}
result = append(result,
[]*boxlayout.Box{
{
Window: "options",
Weight: 1,
},
{
Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
},
}...,
)
return result
}
func (gui *Gui) sideViewNames() []string {
if gui.DockerCommand.InDockerComposeProject {
return []string{"project", "services", "containers", "images", "volumes"}
} else {
return []string{"project", "containers", "images", "volumes"}
}
}
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName()
sideWindowNames := gui.sideViewNames()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: 0,
}
}
}
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
return fullHeightBox(window)
})
} else if height >= 28 {
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
Window: defaultBox.Window,
Weight: 2,
}
}
return defaultBox
}
return append([]*boxlayout.Box{
{
Window: sideWindowNames[0],
Size: 3,
},
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
})...)
} else {
squashedHeight := 1
if height >= 21 {
squashedHeight = 3
}
squashedSidePanelBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: squashedHeight,
}
}
}
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
return squashedSidePanelBox(window)
})
}
}
// TODO: reintroduce
// func (gui *Gui) currentSideWindowName() string {
// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom
// gui.State.ContextManager.RLock()
// defer gui.State.ContextManager.RUnlock()
// for idx := range gui.State.ContextManager.ContextStack {
// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
// context := gui.State.ContextManager.ContextStack[reversedIdx]
// if context.GetKind() == types.SIDE_CONTEXT {
// return context.GetWindowName()
// }
// }
// return "files" // default
// }
func (gui *Gui) currentNonPopupWindowName() string {
return gui.peekPreviousView()
}
// TODO: do this better.
func (gui *Gui) currentSideWindowName() string {
windowName := gui.currentWindow()
if !lo.Contains(gui.sideViewNames(), windowName) {
return gui.peekPreviousView()
}
return windowName
}

@ -73,6 +73,9 @@ type Gui struct {
T *tasks.TaskManager
ErrorChan chan error
CyclableViews []string
Views Views
ViewsSetup bool
}
type servicePanelState struct {
@ -131,8 +134,22 @@ type guiState struct {
// We increment it each time we switch to a new subprocess
// Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose
SessionIndex int
ScreenMode WindowMaximisation
}
// screen sizing determines how much space your selected window takes up (window
// as in panel, not your terminal's window). Sometimes you want a bit more space
// to see the contents of a panel, and this keeps track of how much maximisation
// you've set
type WindowMaximisation int
const (
SCREEN_NORMAL WindowMaximisation = iota
SCREEN_HALF
SCREEN_FULL
)
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
initialState := guiState{

@ -506,6 +506,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.scrollRightMain,
},
{
ViewName: "",
Key: '+',
Handler: wrappedHandler(gui.nextScreenMode),
Description: gui.Tr.LcNextScreenMode,
},
{
ViewName: "",
Key: '_',
Handler: wrappedHandler(gui.prevScreenMode),
Description: gui.Tr.LcPrevScreenMode,
},
}
// TODO: add more views here
@ -573,3 +585,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
return nil
}
func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}

@ -1,11 +1,11 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
@ -59,204 +59,61 @@ func (gui *Gui) onFocus(v *gocui.View) {
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.Version
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
information = donate + " " + information
}
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.NotEnoughSpace
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
}
return nil
}
currView := gui.g.CurrentView()
currentCyclebleView := gui.peekPreviousView()
if currView != nil {
viewName := currView.Name()
usePreviouseView := true
for _, view := range gui.CyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviouseView = false
break
}
}
if usePreviouseView {
currentCyclebleView = gui.peekPreviousView()
}
}
usableSpace := height - 4
tallPanels := 3
var vHeights map[string]int
if gui.DockerCommand.InDockerComposeProject {
tallPanels++
vHeights = map[string]int{
"project": 3,
"services": usableSpace/tallPanels + usableSpace%tallPanels,
"containers": usableSpace / tallPanels,
"images": usableSpace / tallPanels,
"volumes": usableSpace / tallPanels,
"options": 1,
}
} else {
vHeights = map[string]int{
"project": 3,
"containers": usableSpace/tallPanels + usableSpace%tallPanels,
"images": usableSpace / tallPanels,
"volumes": usableSpace / tallPanels,
"options": 1,
if !gui.ViewsSetup {
if err := gui.createAllViews(); err != nil {
return err
}
}
if height < 28 {
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights = map[string]int{
"project": defaultHeight,
"containers": defaultHeight,
"images": defaultHeight,
"volumes": defaultHeight,
"options": defaultHeight,
}
if gui.DockerCommand.InDockerComposeProject {
vHeights["services"] = defaultHeight
}
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
gui.ViewsSetup = true
}
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
leftSideWidth := width / 3
g.Highlight = true
width, height := g.Size()
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
_, _ = g.SetViewOnBottom("limit")
_ = g.DeleteView("limit")
v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
v.FgColor = gocui.ColorDefault
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
v.IgnoreCarriageReturns = true
}
if v, err := g.SetView("project", 0, 0, leftSideWidth, vHeights["project"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.ProjectTitle
v.FgColor = gocui.ColorDefault
}
viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus)
// we assume that the view has already been created.
setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) {
dimensionsObj, ok := viewDimensions[windowName]
var servicesView *gocui.View
aboveContainersView := "project"
if gui.DockerCommand.InDockerComposeProject {
aboveContainersView = "services"
servicesView, err = g.SetViewBeneath("services", "project", vHeights["services"])
view, err := g.View(viewName)
if err != nil {
if err.Error() != "unknown view" {
return err
}
servicesView.Highlight = true
servicesView.Title = gui.Tr.ServicesTitle
servicesView.FgColor = gocui.ColorDefault
}
}
containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
containersView.Highlight = true
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
containersView.Title = gui.Tr.ContainersTitle
} else {
containersView.Title = gui.Tr.StandaloneContainersTitle
}
containersView.FgColor = gocui.ColorDefault
}
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
if err != nil {
if err.Error() != "unknown view" {
return err
return nil, err
}
imagesView.Highlight = true
imagesView.Title = gui.Tr.ImagesTitle
imagesView.FgColor = gocui.ColorDefault
}
volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"])
if err != nil {
if err.Error() != "unknown view" {
return err
if !ok {
// view not specified in dimensions object: so create the view and hide it
// making the view take up the whole space in the background in case it needs
// to render content as soon as it appears, because lazyloaded content (via a pty task)
// cares about the size of the view.
_, err := g.SetView(viewName, 0, 0, width, height, 0)
view.Visible = false
return view, err
}
volumesView.Highlight = true
volumesView.Title = gui.Tr.VolumesTitle
volumesView.FgColor = gocui.ColorDefault
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
frameOffset := 1
if view.Frame {
frameOffset = 0
}
v.Frame = false
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
_, err = g.SetView(
viewName,
dimensionsObj.X0-frameOffset,
dimensionsObj.Y0-frameOffset,
dimensionsObj.X1+frameOffset,
dimensionsObj.Y1+frameOffset,
0,
)
view.Visible = true
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
return view, err
}
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "information", information); err != nil {
for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} {
_, err := setViewFromDimensions(viewName, viewName)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.waitForIntro.Done()
}
if gui.g.CurrentView() == nil {
@ -303,3 +160,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
gui.focusY(state.selectedLine, state.lineCount, view)
}
}
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
// arbitrarily giving the view enough size so that we don't get an error, but
// it's expected that the view will be given the correct size before being shown
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
}

@ -5,8 +5,8 @@ import (
)
// GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil
func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute {
return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor)
}
// SetColorScheme sets the color scheme for the app based on the user config

@ -291,6 +291,10 @@ func (gui *Gui) trimmedContent(v *gocui.View) string {
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
// this can happen when the app is first starting up
if currentView == nil {
return gui.initiallyFocusedViewName()
}
return currentView.Name()
}
@ -383,3 +387,51 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han
return handleSelect(gui.g, v)
}
func (gui *Gui) nextScreenMode() error {
if gui.currentViewName() == "main" {
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
func (gui *Gui) prevScreenMode() error {
if gui.currentViewName() == "main" {
gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return nil
}
func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
return sl[0]
}
func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}

@ -0,0 +1,128 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/samber/lo"
)
type Views struct {
Project *gocui.View
Services *gocui.View
Containers *gocui.View
Images *gocui.View
Volumes *gocui.View
Main *gocui.View
Options *gocui.View
Confirmation *gocui.View
Menu *gocui.View
Information *gocui.View
AppStatus *gocui.View
Limit *gocui.View
}
type viewNameMapping struct {
viewPtr **gocui.View
name string
}
func (gui *Gui) orderedViews() []*gocui.View {
return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View {
return *v.viewPtr
})
}
func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
return []viewNameMapping{
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
{viewPtr: &gui.Views.Project, name: "project"},
{viewPtr: &gui.Views.Services, name: "services"},
{viewPtr: &gui.Views.Containers, name: "containers"},
{viewPtr: &gui.Views.Images, name: "images"},
{viewPtr: &gui.Views.Volumes, name: "volumes"},
{viewPtr: &gui.Views.Main, name: "main"},
// bottom line
{viewPtr: &gui.Views.Options, name: "options"},
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
{viewPtr: &gui.Views.Information, name: "information"},
// popups.
{viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
// this guy will cover everything else when it appears
{viewPtr: &gui.Views.Limit, name: "limit"},
}
}
func (gui *Gui) createAllViews() error {
var err error
for _, mapping := range gui.orderedViewNameMappings() {
*mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
(*mapping.viewPtr).FgColor = gocui.ColorDefault
}
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Project.Title = gui.Tr.ProjectTitle
gui.Views.Services.Highlight = true
gui.Views.Services.Title = gui.Tr.ServicesTitle
gui.Views.Containers.Highlight = true
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
gui.Views.Containers.Title = gui.Tr.ContainersTitle
} else {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
}
gui.Views.Images.Highlight = true
gui.Views.Images.Title = gui.Tr.ImagesTitle
gui.Views.Volumes.Highlight = true
gui.Views.Volumes.Title = gui.Tr.VolumesTitle
gui.Views.Options.Frame = false
gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor()
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Frame = false
gui.Views.Information.Frame = false
gui.Views.Information.FgColor = gocui.ColorGreen
if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil {
return err
}
gui.Views.Confirmation.Visible = false
gui.Views.Menu.Visible = false
gui.Views.Limit.Visible = false
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
gui.waitForIntro.Done()
return nil
}
func (gui *Gui) getInformationContent() string {
informationStr := gui.Config.Version
if !gui.g.Mouse {
return informationStr
}
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
return donate + " " + informationStr
}

@ -0,0 +1,7 @@
package gui
func (gui *Gui) currentWindow() string {
// at the moment, we only have one view per window in lazydocker, so we
// are using the view name as the window name
return gui.currentViewName()
}

@ -105,6 +105,9 @@ type TranslationSet struct {
No string
Yes string
LcNextScreenMode string
LcPrevScreenMode string
}
func englishSet() TranslationSet {
@ -217,5 +220,8 @@ func englishSet() TranslationSet {
No: "no",
Yes: "yes",
LcNextScreenMode: "next screen mode (normal/half/fullscreen)",
LcPrevScreenMode: "prev screen mode",
}
}

@ -0,0 +1,5 @@
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)

@ -0,0 +1,5 @@
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
GOPROXY=direct go get -u github.com/jesseduffield/lazycore@master && go mod vendor && go mod tidy
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Jesse Duffield
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,211 @@
package boxlayout
import (
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/samber/lo"
)
type Dimensions struct {
X0 int
X1 int
Y0 int
Y1 int
}
type Direction int
const (
ROW Direction = iota
COLUMN
)
// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space.
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
// If a box represents a window, you can put the window name in the Window field.
// When determining how to divvy-up the available height (for row children) or width (for column children), we first
// give the boxes with a static `size` the space that they want. Then we apportion
// the remaining space based on the weights of the dynamic boxes (you can't define
// both size and weight at the same time: you gotta pick one). If there are two
// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
// of the available space and the second one gets the remaining 66%
type Box struct {
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
Direction Direction
// function which takes the width and height assigned to the box and decides which orientation it will have
ConditionalDirection func(width int, height int) Direction
Children []*Box
// function which takes the width and height assigned to the box and decides the layout of the children.
ConditionalChildren func(width int, height int) []*Box
// Window refers to the name of the window this box represents, if there is one
Window string
// static Size. If parent box's direction is ROW this refers to height, otherwise width
Size int
// dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box
// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
Weight int
}
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
children := root.getChildren(width, height)
if len(children) == 0 {
// leaf node
if root.Window != "" {
dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
return map[string]Dimensions{root.Window: dimensionsForWindow}
}
return map[string]Dimensions{}
}
direction := root.getDirection(width, height)
var availableSize int
if direction == COLUMN {
availableSize = width
} else {
availableSize = height
}
sizes := calcSizes(children, availableSize)
result := map[string]Dimensions{}
offset := 0
for i, child := range children {
boxSize := sizes[i]
var resultForChild map[string]Dimensions
if direction == COLUMN {
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
} else {
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
}
result = mergeDimensionMaps(result, resultForChild)
offset += boxSize
}
return result
}
func calcSizes(boxes []*Box, availableSpace int) []int {
normalizedWeights := normalizeWeights(lo.Map(boxes, func(box *Box, _ int) int { return box.Weight }))
totalWeight := 0
reservedSpace := 0
for i, box := range boxes {
if box.isStatic() {
reservedSpace += box.Size
} else {
totalWeight += normalizedWeights[i]
}
}
dynamicSpace := utils.Max(0, availableSpace-reservedSpace)
unitSize := 0
extraSpace := 0
if totalWeight > 0 {
unitSize = dynamicSpace / totalWeight
extraSpace = dynamicSpace % totalWeight
}
result := make([]int, len(boxes))
for i, box := range boxes {
if box.isStatic() {
// assuming that only one static child can have a size greater than the
// available space. In that case we just crop the size to what's available
result[i] = utils.Min(availableSpace, box.Size)
} else {
result[i] = unitSize * normalizedWeights[i]
}
}
// distribute the remainder across dynamic boxes.
for extraSpace > 0 {
for i, weight := range normalizedWeights {
if weight > 0 {
result[i]++
extraSpace--
normalizedWeights[i]--
if extraSpace == 0 {
break
}
}
}
}
return result
}
// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2.
func normalizeWeights(weights []int) []int {
if len(weights) == 0 {
return []int{}
}
// to spare us some computation we'll exit early if any of our weights is 1
if lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) {
return weights
}
// map weights to factorSlices and find the lowest common factor
positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 })
factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) })
commonFactors := factorSlices[0]
for _, factors := range factorSlices {
commonFactors = lo.Intersect(commonFactors, factors)
}
if len(commonFactors) == 0 {
return weights
}
newWeights := lo.Map(weights, func(weight int, _ int) int { return weight / commonFactors[0] })
return normalizeWeights(newWeights)
}
func calcFactors(n int) []int {
factors := []int{}
for i := 2; i <= n; i++ {
if n%i == 0 {
factors = append(factors, i)
}
}
return factors
}
func (b *Box) isStatic() bool {
return b.Size > 0
}
func (b *Box) getDirection(width int, height int) Direction {
if b.ConditionalDirection != nil {
return b.ConditionalDirection(width, height)
}
return b.Direction
}
func (b *Box) getChildren(width int, height int) []*Box {
if b.ConditionalChildren != nil {
return b.ConditionalChildren(width, height)
}
return b.Children
}
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
result := map[string]Dimensions{}
for _, dimensionMap := range []map[string]Dimensions{a, b} {
for k, v := range dimensionMap {
result[k] = v
}
}
return result
}

@ -0,0 +1,16 @@
package utils
// Min returns the minimum of two integers
func Min(x, y int) int {
if x < y {
return x
}
return y
}
func Max(x, y int) int {
if x > y {
return x
}
return y
}

@ -2,6 +2,128 @@
@samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade.
## 1.31.0 (2022-10-06)
Adding:
- lo.SliceToChannel
- lo.Generator
- lo.Batch
- lo.BatchWithTimeout
## 1.30.1 (2022-10-06)
Fix:
- lo.Try1: remove generic type
- lo.Validate: format error properly
## 1.30.0 (2022-10-04)
Adding:
- lo.TernaryF
- lo.Validate
## 1.29.0 (2022-10-02)
Adding:
- lo.ErrorAs
- lo.TryOr
- lo.TryOrX
## 1.28.0 (2022-09-05)
Adding:
- lo.ChannelDispatcher with 6 dispatching strategies:
- lo.DispatchingStrategyRoundRobin
- lo.DispatchingStrategyRandom
- lo.DispatchingStrategyWeightedRandom
- lo.DispatchingStrategyFirst
- lo.DispatchingStrategyLeast
- lo.DispatchingStrategyMost
## 1.27.1 (2022-08-15)
Bugfix:
- Removed comparable constraint for lo.FindKeyBy
## 1.27.0 (2022-07-29)
Breaking:
- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R`
Added:
- lo.ChunkString
- lo.SliceToMap (alias to lo.Associate)
## 1.26.0 (2022-07-24)
Adding:
- lo.Associate
- lo.ReduceRight
- lo.FromPtrOr
- lo.MapToSlice
- lo.IsSorted
- lo.IsSortedByKey
## 1.25.0 (2022-07-04)
Adding:
- lo.FindUniques
- lo.FindUniquesBy
- lo.FindDuplicates
- lo.FindDuplicatesBy
- lo.IsNotEmpty
## 1.24.0 (2022-07-04)
Adding:
- lo.Without
- lo.WithoutEmpty
## 1.23.0 (2022-07-04)
Adding:
- lo.FindKey
- lo.FindKeyBy
## 1.22.0 (2022-07-04)
Adding:
- lo.Slice
- lo.FromPtr
- lo.IsEmpty
- lo.Compact
- lo.ToPairs: alias to lo.Entries
- lo.FromPairs: alias to lo.FromEntries
- lo.Partial
Change:
- lo.Must + lo.MustX: add context to panic message
Fix:
- lo.Nth: out of bound exception (#137)
## 1.21.0 (2022-05-10)
Adding:
- lo.ToAnySlice
- lo.FromAnySlice
## 1.20.0 (2022-05-02)
Adding:
@ -55,7 +177,7 @@ Improvement:
Adding:
- lo.Colaesce
- lo.Coalesce
## 1.13.0 (2022-04-14)

@ -1,10 +1,5 @@
BIN=go
# BIN=go1.18beta1
go1.18beta1:
go install golang.org/dl/go1.18beta1@latest
go1.18beta1 download
build:
${BIN} build -v ./...
@ -20,7 +15,7 @@ watch-bench:
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
coverage:
${BIN} test -v -coverprofile cover.out .
${BIN} test -v -coverprofile=cover.out -covermode=atomic .
${BIN} tool cover -html=cover.out -o cover.html
# tools
@ -31,7 +26,7 @@ tools:
${BIN} install github.com/jondot/goweight@latest
${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
${BIN} get -t -u golang.org/x/tools/cmd/cover
${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest
${BIN} install github.com/sonatype-nexus-community/nancy@latest
go mod tidy
lint:
@ -40,11 +35,9 @@ lint-fix:
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
audit: tools
${BIN} mod tidy
${BIN} list -json -m all | nancy sleuth
outdated: tools
${BIN} mod tidy
${BIN} list -u -m -json all | go-mod-outdated -update -direct
weight: tools

File diff suppressed because it is too large Load Diff

@ -0,0 +1,228 @@
package lo
import (
"math/rand"
"time"
)
type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int
// ChannelDispatcher distributes messages from input channels into N child channels.
// Close events are propagated to children.
// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0.
func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T {
children := createChannels[T](count, channelBufferCap)
roChildren := channelsToReadOnly(children)
go func() {
// propagate channel closing to children
defer closeChannels(children)
var i uint64 = 0
for {
msg, ok := <-stream
if !ok {
return
}
destination := strategy(msg, i, roChildren) % count
children[destination] <- msg
i++
}
}()
return roChildren
}
func createChannels[T any](count int, channelBufferCap int) []chan T {
children := make([]chan T, 0, count)
for i := 0; i < count; i++ {
children = append(children, make(chan T, channelBufferCap))
}
return children
}
func channelsToReadOnly[T any](children []chan T) []<-chan T {
roChildren := make([]<-chan T, 0, len(children))
for i := range children {
roChildren = append(roChildren, children[i])
}
return roChildren
}
func closeChannels[T any](children []chan T) {
for i := 0; i < len(children); i++ {
close(children[i])
}
}
func channelIsNotFull[T any](ch <-chan T) bool {
return cap(ch) == 0 || len(ch) < cap(ch)
}
// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner.
// If the channel capacity is exceeded, the next channel will be selected and so on.
func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int {
for {
i := int(index % uint64(len(channels)))
if channelIsNotFull(channels[i]) {
return i
}
index++
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
}
}
// DispatchingStrategyRandom distributes messages in a random manner.
// If the channel capacity is exceeded, another random channel will be selected and so on.
func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int {
for {
i := rand.Intn(len(channels))
if channelIsNotFull(channels[i]) {
return i
}
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
}
}
// DispatchingStrategyRandom distributes messages in a weighted manner.
// If the channel capacity is exceeded, another random channel will be selected and so on.
func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] {
seq := []int{}
for i := 0; i < len(weights); i++ {
for j := 0; j < weights[i]; j++ {
seq = append(seq, i)
}
}
return func(msg T, index uint64, channels []<-chan T) int {
for {
i := seq[rand.Intn(len(seq))]
if channelIsNotFull(channels[i]) {
return i
}
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
}
}
}
// DispatchingStrategyFirst distributes messages in the first non-full channel.
// If the capacity of the first channel is exceeded, the second channel will be selected and so on.
func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int {
for {
for i := range channels {
if channelIsNotFull(channels[i]) {
return i
}
}
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
}
}
// DispatchingStrategyLeast distributes messages in the emptiest channel.
func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int {
seq := Range(len(channels))
return MinBy(seq, func(item int, min int) bool {
return len(channels[item]) < len(channels[min])
})
}
// DispatchingStrategyMost distributes messages in the fulliest channel.
// If the channel capacity is exceeded, the next channel will be selected and so on.
func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int {
seq := Range(len(channels))
return MaxBy(seq, func(item int, max int) bool {
return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item])
})
}
// SliceToChannel returns a read-only channels of collection elements.
func SliceToChannel[T any](bufferSize int, collection []T) <-chan T {
ch := make(chan T, bufferSize)
go func() {
for _, item := range collection {
ch <- item
}
close(ch)
}()
return ch
}
// Generator implements the generator design pattern.
func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T {
ch := make(chan T, bufferSize)
go func() {
// WARNING: infinite loop
generator(func(t T) {
ch <- t
})
close(ch)
}()
return ch
}
// Batch creates a slice of n elements from a channel. Returns the slice and the slice length.
// @TODO: we should probaby provide an helper that reuse the same buffer.
func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
buffer := make([]T, 0, size)
index := 0
now := time.Now()
for ; index < size; index++ {
item, ok := <-ch
if !ok {
return buffer, index, time.Since(now), false
}
buffer = append(buffer, item)
}
return buffer, index, time.Since(now), true
}
// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length.
// @TODO: we should probaby provide an helper that reuse the same buffer.
func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) {
expire := time.NewTimer(timeout)
defer expire.Stop()
buffer := make([]T, 0, size)
index := 0
now := time.Now()
for ; index < size; index++ {
select {
case item, ok := <-ch:
if !ok {
return buffer, index, time.Since(now), false
}
buffer = append(buffer, item)
case <-expire.C:
return buffer, index, time.Since(now), true
}
}
return buffer, index, time.Since(now), true
}

@ -1,6 +1,7 @@
package lo
// Ternary is a 1 line if/else statement.
// Play: https://go.dev/play/p/t-D7WBL44h2
func Ternary[T any](condition bool, ifOutput T, elseOutput T) T {
if condition {
return ifOutput
@ -9,12 +10,23 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T {
return elseOutput
}
// TernaryF is a 1 line if/else statement whose options are functions
// Play: https://go.dev/play/p/AO4VW20JoqM
func TernaryF[T any](condition bool, ifFunc func() T, elseFunc func() T) T {
if condition {
return ifFunc()
}
return elseFunc()
}
type ifElse[T any] struct {
result T
done bool
}
// If.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func If[T any](condition bool, result T) *ifElse[T] {
if condition {
return &ifElse[T]{result, true}
@ -25,6 +37,7 @@ func If[T any](condition bool, result T) *ifElse[T] {
}
// IfF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func IfF[T any](condition bool, resultF func() T) *ifElse[T] {
if condition {
return &ifElse[T]{resultF(), true}
@ -35,6 +48,7 @@ func IfF[T any](condition bool, resultF func() T) *ifElse[T] {
}
// ElseIf.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] {
if !i.done && condition {
i.result = result
@ -45,6 +59,7 @@ func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] {
}
// ElseIfF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] {
if !i.done && condition {
i.result = resultF()
@ -55,6 +70,7 @@ func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] {
}
// Else.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) Else(result T) T {
if i.done {
return i.result
@ -64,6 +80,7 @@ func (i *ifElse[T]) Else(result T) T {
}
// ElseF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseF(resultF func() T) T {
if i.done {
return i.result
@ -79,6 +96,7 @@ type switchCase[T comparable, R any] struct {
}
// Switch is a pure functional switch/case/default statement.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func Switch[T comparable, R any](predicate T) *switchCase[T, R] {
var result R
@ -90,6 +108,7 @@ func Switch[T comparable, R any](predicate T) *switchCase[T, R] {
}
// Case.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
if !s.done && s.predicate == val {
s.result = result
@ -100,6 +119,7 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
}
// CaseF.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] {
if !s.done && s.predicate == val {
s.result = cb()
@ -110,6 +130,7 @@ func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] {
}
// Default.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) Default(result R) R {
if !s.done {
s.result = result
@ -119,6 +140,7 @@ func (s *switchCase[T, R]) Default(result R) R {
}
// DefaultF.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) DefaultF(cb func() R) R {
if !s.done {
s.result = cb()

@ -2,8 +2,8 @@ version: '3'
services:
dev:
build: .
image: golang:1.18-bullseye
volumes:
- ./:/go/src/github.com/samber/lo
working_dir: /go/src/github.com/samber/lo
command: bash -c 'make tools ; make watch-test'
command: make watch-test

@ -1,62 +1,115 @@
package lo
import (
"errors"
"fmt"
"reflect"
)
// Validate is a helper that creates an error when a condition is not met.
// Play: https://go.dev/play/p/vPyh51XpCBt
func Validate(ok bool, format string, args ...any) error {
if !ok {
return fmt.Errorf(fmt.Sprintf(format, args...))
}
return nil
}
func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
if len(msgAndArgs) == 1 {
if msgAsStr, ok := msgAndArgs[0].(string); ok {
return msgAsStr
}
return fmt.Sprintf("%+v", msgAndArgs[0])
}
if len(msgAndArgs) > 1 {
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
}
return ""
}
// must panics if err is error or false.
func must(err any) {
b, isBool := err.(bool)
if isBool && !b {
panic("not ok")
func must(err any, messageArgs ...interface{}) {
if err == nil {
return
}
e, isError := err.(error)
if isError {
panic(e)
switch e := err.(type) {
case bool:
if !e {
message := messageFromMsgAndArgs(messageArgs...)
if message == "" {
message = "not ok"
}
panic(message)
}
case error:
message := messageFromMsgAndArgs(messageArgs...)
if message != "" {
panic(message + ": " + e.Error())
} else {
panic(e.Error())
}
default:
panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error")
}
}
// Must is a helper that wraps a call to a function returning a value and an error
// and panics if err is error or false.
func Must[T any](val T, err any) T {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must[T any](val T, err any, messageArgs ...interface{}) T {
must(err, messageArgs...)
return val
}
// Must0 has the same behavior than Must, but callback returns no variable.
func Must0(err any) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must0(err any, messageArgs ...interface{}) {
must(err, messageArgs...)
}
// Must1 is an alias to Must
func Must1[T any](val T, err any) T {
return Must(val, err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must1[T any](val T, err any, messageArgs ...interface{}) T {
return Must(val, err, messageArgs...)
}
// Must2 has the same behavior than Must, but callback returns 2 variables.
func Must2[T1 any, T2 any](val1 T1, val2 T2, err any) (T1, T2) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) {
must(err, messageArgs...)
return val1, val2
}
// Must3 has the same behavior than Must, but callback returns 3 variables.
func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any) (T1, T2, T3) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) {
must(err, messageArgs...)
return val1, val2, val3
}
// Must4 has the same behavior than Must, but callback returns 4 variables.
func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any) (T1, T2, T3, T4) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) {
must(err, messageArgs...)
return val1, val2, val3, val4
}
// Must5 has the same behavior than Must, but callback returns 5 variables.
func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any) (T1, T2, T3, T4, T5) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) {
must(err, messageArgs...)
return val1, val2, val3, val4, val5
}
// Must6 has the same behavior than Must, but callback returns 6 variables.
func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any) (T1, T2, T3, T4, T5, T6) {
must(err)
// Play: https://go.dev/play/p/TMoWrRp3DyC
func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) {
must(err, messageArgs...)
return val1, val2, val3, val4, val5, val6
}
@ -79,6 +132,7 @@ func Try(callback func() error) (ok bool) {
}
// Try0 has the same behavior than Try, but callback returns no variable.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try0(callback func()) bool {
return Try(func() error {
callback()
@ -87,11 +141,13 @@ func Try0(callback func()) bool {
}
// Try1 is an alias to Try.
func Try1[T any](callback func() error) bool {
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try1(callback func() error) bool {
return Try(callback)
}
// Try2 has the same behavior than Try, but callback returns 2 variables.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try2[T any](callback func() (T, error)) bool {
return Try(func() error {
_, err := callback()
@ -100,6 +156,7 @@ func Try2[T any](callback func() (T, error)) bool {
}
// Try3 has the same behavior than Try, but callback returns 3 variables.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try3[T, R any](callback func() (T, R, error)) bool {
return Try(func() error {
_, _, err := callback()
@ -108,6 +165,7 @@ func Try3[T, R any](callback func() (T, R, error)) bool {
}
// Try4 has the same behavior than Try, but callback returns 4 variables.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try4[T, R, S any](callback func() (T, R, S, error)) bool {
return Try(func() error {
_, _, _, err := callback()
@ -116,6 +174,7 @@ func Try4[T, R, S any](callback func() (T, R, S, error)) bool {
}
// Try5 has the same behavior than Try, but callback returns 5 variables.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool {
return Try(func() error {
_, _, _, _, err := callback()
@ -124,6 +183,7 @@ func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool {
}
// Try6 has the same behavior than Try, but callback returns 6 variables.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool {
return Try(func() error {
_, _, _, _, _, err := callback()
@ -131,7 +191,125 @@ func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool {
})
}
// TryOr has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) {
return TryOr1(callback, fallbackA)
}
// TryOr1 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) {
ok := false
Try0(func() {
a, err := callback()
if err == nil {
fallbackA = a
ok = true
}
})
return fallbackA, ok
}
// TryOr2 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr2[A any, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) {
ok := false
Try0(func() {
a, b, err := callback()
if err == nil {
fallbackA = a
fallbackB = b
ok = true
}
})
return fallbackA, fallbackB, ok
}
// TryOr3 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr3[A any, B any, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) {
ok := false
Try0(func() {
a, b, c, err := callback()
if err == nil {
fallbackA = a
fallbackB = b
fallbackC = c
ok = true
}
})
return fallbackA, fallbackB, fallbackC, ok
}
// TryOr4 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr4[A any, B any, C any, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) {
ok := false
Try0(func() {
a, b, c, d, err := callback()
if err == nil {
fallbackA = a
fallbackB = b
fallbackC = c
fallbackD = d
ok = true
}
})
return fallbackA, fallbackB, fallbackC, fallbackD, ok
}
// TryOr5 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr5[A any, B any, C any, D any, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) {
ok := false
Try0(func() {
a, b, c, d, e, err := callback()
if err == nil {
fallbackA = a
fallbackB = b
fallbackC = c
fallbackD = d
fallbackE = e
ok = true
}
})
return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok
}
// TryOr6 has the same behavior than Must, but returns a default value in case of error.
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
func TryOr6[A any, B any, C any, D any, E any, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) {
ok := false
Try0(func() {
a, b, c, d, e, f, err := callback()
if err == nil {
fallbackA = a
fallbackB = b
fallbackC = c
fallbackD = d
fallbackE = e
fallbackF = f
ok = true
}
})
return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok
}
// TryWithErrorValue has the same behavior than Try, but also returns value passed to panic.
// Play: https://go.dev/play/p/Kc7afQIT2Fs
func TryWithErrorValue(callback func() error) (errorValue any, ok bool) {
ok = true
@ -152,6 +330,7 @@ func TryWithErrorValue(callback func() error) (errorValue any, ok bool) {
}
// TryCatch has the same behavior than Try, but calls the catch function in case of error.
// Play: https://go.dev/play/p/PnOON-EqBiU
func TryCatch(callback func() error, catch func()) {
if !Try(callback) {
catch()
@ -159,8 +338,17 @@ func TryCatch(callback func() error, catch func()) {
}
// TryCatchWithErrorValue has the same behavior than TryWithErrorValue, but calls the catch function in case of error.
// Play: https://go.dev/play/p/8Pc9gwX_GZO
func TryCatchWithErrorValue(callback func() error, catch func(any)) {
if err, ok := TryWithErrorValue(callback); !ok {
catch(err)
}
}
// ErrorsAs is a shortcut for errors.As(err, &&T).
// Play: https://go.dev/play/p/8wk5rH8UfrE
func ErrorsAs[T error](err error) (T, bool) {
var t T
ok := errors.As(err, &t)
return t, ok
}

158
vendor/github.com/samber/lo/find.go generated vendored

@ -2,7 +2,6 @@ package lo
import (
"fmt"
"math"
"math/rand"
"golang.org/x/exp/constraints"
@ -87,6 +86,140 @@ func FindOrElse[T any](collection []T, fallback T, predicate func(T) bool) T {
return fallback
}
// FindKey returns the key of the first value matching.
func FindKey[K comparable, V comparable](object map[K]V, value V) (K, bool) {
for k, v := range object {
if v == value {
return k, true
}
}
return Empty[K](), false
}
// FindKeyBy returns the key of the first element predicate returns truthy for.
func FindKeyBy[K comparable, V any](object map[K]V, predicate func(K, V) bool) (K, bool) {
for k, v := range object {
if predicate(k, v) {
return k, true
}
}
return Empty[K](), false
}
// FindUniques returns a slice with all the unique elements of the collection.
// The order of result values is determined by the order they occur in the collection.
func FindUniques[T comparable](collection []T) []T {
isDupl := make(map[T]bool, len(collection))
for _, item := range collection {
duplicated, ok := isDupl[item]
if !ok {
isDupl[item] = false
} else if !duplicated {
isDupl[item] = true
}
}
result := make([]T, 0, len(collection)-len(isDupl))
for _, item := range collection {
if duplicated := isDupl[item]; !duplicated {
result = append(result, item)
}
}
return result
}
// FindUniquesBy returns a slice with all the unique elements of the collection.
// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is
// invoked for each element in array to generate the criterion by which uniqueness is computed.
func FindUniquesBy[T any, U comparable](collection []T, iteratee func(T) U) []T {
isDupl := make(map[U]bool, len(collection))
for _, item := range collection {
key := iteratee(item)
duplicated, ok := isDupl[key]
if !ok {
isDupl[key] = false
} else if !duplicated {
isDupl[key] = true
}
}
result := make([]T, 0, len(collection)-len(isDupl))
for _, item := range collection {
key := iteratee(item)
if duplicated := isDupl[key]; !duplicated {
result = append(result, item)
}
}
return result
}
// FindDuplicates returns a slice with the first occurence of each duplicated elements of the collection.
// The order of result values is determined by the order they occur in the collection.
func FindDuplicates[T comparable](collection []T) []T {
isDupl := make(map[T]bool, len(collection))
for _, item := range collection {
duplicated, ok := isDupl[item]
if !ok {
isDupl[item] = false
} else if !duplicated {
isDupl[item] = true
}
}
result := make([]T, 0, len(collection)-len(isDupl))
for _, item := range collection {
if duplicated := isDupl[item]; duplicated {
result = append(result, item)
isDupl[item] = false
}
}
return result
}
// FindDuplicatesBy returns a slice with the first occurence of each duplicated elements of the collection.
// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is
// invoked for each element in array to generate the criterion by which uniqueness is computed.
func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(T) U) []T {
isDupl := make(map[U]bool, len(collection))
for _, item := range collection {
key := iteratee(item)
duplicated, ok := isDupl[key]
if !ok {
isDupl[key] = false
} else if !duplicated {
isDupl[key] = true
}
}
result := make([]T, 0, len(collection)-len(isDupl))
for _, item := range collection {
key := iteratee(item)
if duplicated := isDupl[key]; duplicated {
result = append(result, item)
isDupl[key] = false
}
}
return result
}
// Min search the minimum value of a collection.
func Min[T constraints.Ordered](collection []T) T {
var min T
@ -187,19 +320,18 @@ func Last[T any](collection []T) (T, error) {
// Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element
// from the end is returned. An error is returned when nth is out of slice bounds.
func Nth[T any](collection []T, nth int) (T, error) {
if int(math.Abs(float64(nth))) >= len(collection) {
func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) {
n := int(nth)
l := len(collection)
if n >= l || -n > l {
var t T
return t, fmt.Errorf("nth: %d out of slice bounds", nth)
return t, fmt.Errorf("nth: %d out of slice bounds", n)
}
length := len(collection)
if nth >= 0 {
return collection[nth], nil
if n >= 0 {
return collection[n], nil
}
return collection[length+nth], nil
return collection[l+n], nil
}
// Sample returns a random item from collection.
@ -216,11 +348,7 @@ func Sample[T any](collection []T) T {
func Samples[T any](collection []T, count int) []T {
size := len(collection)
// put values into a map, for faster deletion
cOpy := make([]T, 0, size)
for _, v := range collection {
cOpy = append(cOpy, v)
}
cOpy := append([]T{}, collection...)
results := []T{}

@ -0,0 +1,8 @@
package lo
// Partial returns new function that, when called, has its first argument set to the provided value.
func Partial[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R {
return func(t2 T2) R {
return f(arg1, t2)
}
}

@ -175,3 +175,28 @@ func Union[T comparable](list1 []T, list2 []T) []T {
return result
}
// Without returns slice excluding all given values.
func Without[T comparable](collection []T, exclude ...T) []T {
result := make([]T, 0, len(collection))
for _, e := range collection {
if !Contains(exclude, e) {
result = append(result, e)
}
}
return result
}
// WithoutEmpty returns slice excluding empty values.
func WithoutEmpty[T comparable](collection []T) []T {
var empty T
result := make([]T, 0, len(collection))
for _, e := range collection {
if e != empty {
result = append(result, e)
}
}
return result
}

@ -1,6 +1,7 @@
package lo
// Keys creates an array of the map keys.
// Play: https://go.dev/play/p/Uu11fHASqrU
func Keys[K comparable, V any](in map[K]V) []K {
result := make([]K, 0, len(in))
@ -12,6 +13,7 @@ func Keys[K comparable, V any](in map[K]V) []K {
}
// Values creates an array of the map values.
// Play: https://go.dev/play/p/nnRTQkzQfF6
func Values[K comparable, V any](in map[K]V) []V {
result := make([]V, 0, len(in))
@ -23,6 +25,7 @@ func Values[K comparable, V any](in map[K]V) []V {
}
// PickBy returns same map type filtered by given predicate.
// Play: https://go.dev/play/p/kdg8GR_QMmf
func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -34,6 +37,7 @@ func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V
}
// PickByKeys returns same map type filtered by given keys.
// Play: https://go.dev/play/p/R1imbuci9qU
func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -45,6 +49,7 @@ func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
}
// PickByValues returns same map type filtered by given values.
// Play: https://go.dev/play/p/1zdzSvbfsJc
func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -55,7 +60,8 @@ func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
return r
}
// PickBy returns same map type filtered by given predicate.
// OmitBy returns same map type filtered by given predicate.
// Play: https://go.dev/play/p/EtBsR43bdsd
func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -67,6 +73,7 @@ func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V
}
// OmitByKeys returns same map type filtered by given keys.
// Play: https://go.dev/play/p/t1QjCrs-ysk
func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -78,6 +85,7 @@ func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
}
// OmitByValues returns same map type filtered by given values.
// Play: https://go.dev/play/p/9UYZi-hrs8j
func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
r := map[K]V{}
for k, v := range in {
@ -89,6 +97,7 @@ func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
}
// Entries transforms a map into array of key/value pairs.
// Play:
func Entries[K comparable, V any](in map[K]V) []Entry[K, V] {
entries := make([]Entry[K, V], 0, len(in))
@ -102,7 +111,15 @@ func Entries[K comparable, V any](in map[K]V) []Entry[K, V] {
return entries
}
// ToPairs transforms a map into array of key/value pairs.
// Alias of Entries().
// Play: https://go.dev/play/p/3Dhgx46gawJ
func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] {
return Entries(in)
}
// FromEntries transforms an array of key/value pairs into a map.
// Play: https://go.dev/play/p/oIr5KHFGCEN
func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V {
out := map[K]V{}
@ -113,9 +130,17 @@ func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V {
return out
}
// FromPairs transforms an array of key/value pairs into a map.
// Alias of FromEntries().
// Play: https://go.dev/play/p/oIr5KHFGCEN
func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V {
return FromEntries(entries)
}
// Invert creates a map composed of the inverted keys and values. If map
// contains duplicate values, subsequent values overwrite property assignments
// of previous values.
// Play: https://go.dev/play/p/rFQ4rak6iA1
func Invert[K comparable, V comparable](in map[K]V) map[V]K {
out := map[V]K{}
@ -127,6 +152,7 @@ func Invert[K comparable, V comparable](in map[K]V) map[V]K {
}
// Assign merges multiple maps from left to right.
// Play: https://go.dev/play/p/VhwfJOyxf5o
func Assign[K comparable, V any](maps ...map[K]V) map[K]V {
out := map[K]V{}
@ -140,6 +166,7 @@ func Assign[K comparable, V any](maps ...map[K]V) map[K]V {
}
// MapKeys manipulates a map keys and transforms it to a map of another type.
// Play: https://go.dev/play/p/9_4WPIqOetJ
func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) R) map[R]V {
result := map[R]V{}
@ -151,6 +178,7 @@ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K)
}
// MapValues manipulates a map values and transforms it to a map of another type.
// Play: https://go.dev/play/p/T_8xAfvcf0W
func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) map[K]R {
result := map[K]R{}
@ -160,3 +188,15 @@ func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) ma
return result
}
// MapToSlice transforms a map into a slice based on specific iteratee
// Play: https://go.dev/play/p/ZuiCZpDt6LD
func MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R {
result := make([]R, 0, len(in))
for k, v := range in {
result = append(result, iteratee(k, v))
}
return result
}

@ -3,6 +3,7 @@ package lo
import "golang.org/x/exp/constraints"
// Range creates an array of numbers (positive and/or negative) with given length.
// Play: https://go.dev/play/p/0r6VimXAi9H
func Range(elementNum int) []int {
length := If(elementNum < 0, -elementNum).Else(elementNum)
result := make([]int, length)
@ -14,6 +15,7 @@ func Range(elementNum int) []int {
}
// RangeFrom creates an array of numbers from start with specified length.
// Play: https://go.dev/play/p/0r6VimXAi9H
func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T {
length := If(elementNum < 0, -elementNum).Else(elementNum)
result := make([]T, length)
@ -26,6 +28,7 @@ func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum in
// RangeWithSteps creates an array of numbers (positive and/or negative) progressing from start up to, but not including end.
// step set to zero will return empty array.
// Play: https://go.dev/play/p/0r6VimXAi9H
func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T {
result := []T{}
if start == end || step == 0 {
@ -50,6 +53,7 @@ func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step
}
// Clamp clamps number within the inclusive lower and upper bounds.
// Play: https://go.dev/play/p/RU4lJNC2hlI
func Clamp[T constraints.Ordered](value T, min T, max T) T {
if value < min {
return min
@ -59,7 +63,8 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T {
return value
}
// Summarizes the values in a collection.
// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned.
// Play: https://go.dev/play/p/Dz_a_7jN_ca
func SumBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(T) R) R {
var sum R = 0
for _, item := range collection {

@ -1,32 +0,0 @@
package lo
// ToPtr returns a pointer copy of value.
func ToPtr[T any](x T) *T {
return &x
}
// ToSlicePtr returns a slice of pointer copy of value.
func ToSlicePtr[T any](collection []T) []*T {
return Map(collection, func(x T, _ int) *T {
return &x
})
}
// Empty returns an empty value.
func Empty[T any]() T {
var t T
return t
}
// Coalesce returns the first non-empty arguments. Arguments must be comparable.
func Coalesce[T comparable](v ...T) (result T, ok bool) {
for _, e := range v {
if e != result {
result = e
ok = true
return
}
}
return
}

@ -46,6 +46,7 @@ func (d *debounce) cancel() {
}
// NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed.
// Play: https://go.dev/play/p/mz32VMK2nqe
func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
d := &debounce{
after: duration,
@ -60,7 +61,8 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
}, d.cancel
}
// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned.
// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned.
// Play: https://go.dev/play/p/3ggJZ2ZKcMj
func Attempt(maxIteration int, f func(int) error) (int, error) {
var err error
@ -76,9 +78,10 @@ func Attempt(maxIteration int, f func(int) error) (int, error) {
}
// AttemptWithDelay invokes a function N times until it returns valid output,
// with a pause betwwen each call. Returning either the caught error or nil.
// When first argument is less than `1`, the function runs until a sucessfull
// with a pause between each call. Returning either the caught error or nil.
// When first argument is less than `1`, the function runs until a successful
// response is returned.
// Play: https://go.dev/play/p/tVs6CygC7m1
func AttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) error) (int, time.Duration, error) {
var err error

@ -2,9 +2,12 @@ package lo
import (
"math/rand"
"golang.org/x/exp/constraints"
)
// Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for.
// Play: https://go.dev/play/p/Apjg3WeSi7K
func Filter[V any](collection []V, predicate func(V, int) bool) []V {
result := []V{}
@ -18,6 +21,7 @@ func Filter[V any](collection []V, predicate func(V, int) bool) []V {
}
// Map manipulates a slice and transforms it to a slice of another type.
// Play: https://go.dev/play/p/OkPcYAhBo0D
func Map[T any, R any](collection []T, iteratee func(T, int) R) []R {
result := make([]R, len(collection))
@ -32,6 +36,8 @@ func Map[T any, R any](collection []T, iteratee func(T, int) R) []R {
// The callback function should return two values:
// - the result of the mapping operation and
// - whether the result element should be included or not.
//
// Play: https://go.dev/play/p/-AuYXfy7opz
func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []R {
result := []R{}
@ -45,6 +51,7 @@ func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []
}
// FlatMap manipulates a slice and transforms and flattens it to a slice of another type.
// Play: https://go.dev/play/p/YSoYmQTA8-U
func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R {
result := []R{}
@ -57,6 +64,7 @@ func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R {
// Reduce reduces collection to a value which is the accumulated result of running each element in collection
// through accumulator, where each successive invocation is supplied the return value of the previous.
// Play: https://go.dev/play/p/R4UHXZNaaUG
func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R {
for i, item := range collection {
initial = accumulator(initial, item, i)
@ -65,7 +73,18 @@ func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial
return initial
}
// ReduceRight helper is like Reduce except that it iterates over elements of collection from right to left.
// Play: https://go.dev/play/p/Fq3W70l7wXF
func ReduceRight[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R {
for i := len(collection) - 1; i >= 0; i-- {
initial = accumulator(initial, collection[i], i)
}
return initial
}
// ForEach iterates over elements of collection and invokes iteratee for each element.
// Play: https://go.dev/play/p/oofyiUPRf8t
func ForEach[T any](collection []T, iteratee func(T, int)) {
for i, item := range collection {
iteratee(item, i)
@ -74,6 +93,7 @@ func ForEach[T any](collection []T, iteratee func(T, int)) {
// Times invokes the iteratee n times, returning an array of the results of each invocation.
// The iteratee is invoked with index as argument.
// Play: https://go.dev/play/p/vgQj3Glr6lT
func Times[T any](count int, iteratee func(int) T) []T {
result := make([]T, count)
@ -86,6 +106,7 @@ func Times[T any](count int, iteratee func(int) T) []T {
// Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept.
// The order of result values is determined by the order they occur in the array.
// Play: https://go.dev/play/p/DTzbeXZ6iEN
func Uniq[T comparable](collection []T) []T {
result := make([]T, 0, len(collection))
seen := make(map[T]struct{}, len(collection))
@ -105,6 +126,7 @@ func Uniq[T comparable](collection []T) []T {
// UniqBy returns a duplicate-free version of an array, in which only the first occurrence of each element is kept.
// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is
// invoked for each element in array to generate the criterion by which uniqueness is computed.
// Play: https://go.dev/play/p/g42Z3QSb53u
func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T {
result := make([]T, 0, len(collection))
seen := make(map[U]struct{}, len(collection))
@ -124,6 +146,7 @@ func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T {
}
// GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee.
// Play: https://go.dev/play/p/XnQBd_v6brd
func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T {
result := map[U][]T{}
@ -138,22 +161,25 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T
// Chunk returns an array of elements split into groups the length of size. If array can't be split evenly,
// the final chunk will be the remaining elements.
// Play: https://go.dev/play/p/EeKl0AuTehH
func Chunk[T any](collection []T, size int) [][]T {
if size <= 0 {
panic("Second parameter must be greater than 0")
}
result := make([][]T, 0, len(collection)/2+1)
length := len(collection)
chunksNum := len(collection) / size
if len(collection)%size != 0 {
chunksNum += 1
}
for i := 0; i < length; i++ {
chunk := i / size
result := make([][]T, 0, chunksNum)
if i%size == 0 {
result = append(result, make([]T, 0, size))
for i := 0; i < chunksNum; i++ {
last := (i + 1) * size
if last > len(collection) {
last = len(collection)
}
result[chunk] = append(result[chunk], collection[i])
result = append(result, collection[i*size:last])
}
return result
@ -162,6 +188,7 @@ func Chunk[T any](collection []T, size int) [][]T {
// PartitionBy returns an array of elements split into groups. The order of grouped values is
// determined by the order they occur in collection. The grouping is generated from the results
// of running each element of collection through iteratee.
// Play: https://go.dev/play/p/NfQ_nGjkgXW
func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]T {
result := [][]T{}
seen := map[K]int{}
@ -187,17 +214,23 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]
}
// Flatten returns an array a single level deep.
// Play: https://go.dev/play/p/rbp9ORaMpjw
func Flatten[T any](collection [][]T) []T {
result := []T{}
totalLen := 0
for i := range collection {
totalLen += len(collection[i])
}
for _, item := range collection {
result = append(result, item...)
result := make([]T, 0, totalLen)
for i := range collection {
result = append(result, collection[i]...)
}
return result
}
// Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm.
// Play: https://go.dev/play/p/Qp73bnTDnc7
func Shuffle[T any](collection []T) []T {
rand.Shuffle(len(collection), func(i, j int) {
collection[i], collection[j] = collection[j], collection[i]
@ -207,6 +240,7 @@ func Shuffle[T any](collection []T) []T {
}
// Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on.
// Play: https://go.dev/play/p/fhUMLvZ7vS6
func Reverse[T any](collection []T) []T {
length := len(collection)
half := length / 2
@ -220,6 +254,7 @@ func Reverse[T any](collection []T) []T {
}
// Fill fills elements of array with `initial` value.
// Play: https://go.dev/play/p/VwR34GzqEub
func Fill[T Clonable[T]](collection []T, initial T) []T {
result := make([]T, 0, len(collection))
@ -231,6 +266,7 @@ func Fill[T Clonable[T]](collection []T, initial T) []T {
}
// Repeat builds a slice with N copies of initial value.
// Play: https://go.dev/play/p/g3uHXbmc3b6
func Repeat[T Clonable[T]](count int, initial T) []T {
result := make([]T, 0, count)
@ -242,6 +278,7 @@ func Repeat[T Clonable[T]](count int, initial T) []T {
}
// RepeatBy builds a slice with values returned by N calls of callback.
// Play: https://go.dev/play/p/ozZLCtX_hNU
func RepeatBy[T any](count int, predicate func(int) T) []T {
result := make([]T, 0, count)
@ -253,6 +290,7 @@ func RepeatBy[T any](count int, predicate func(int) T) []T {
}
// KeyBy transforms a slice or an array of structs to a map based on a pivot callback.
// Play: https://go.dev/play/p/mdaClUAT-zZ
func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V {
result := make(map[K]V, len(collection))
@ -264,21 +302,55 @@ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V {
return result
}
// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice.
// If any of two pairs would have the same key the last one gets added to the map.
// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array.
// Play: https://go.dev/play/p/WHa2CfMO3Lr
func Associate[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V {
result := make(map[K]V)
for _, t := range collection {
k, v := transform(t)
result[k] = v
}
return result
}
// SliceToMap returns a map containing key-value pairs provided by transform function applied to elements of the given slice.
// If any of two pairs would have the same key the last one gets added to the map.
// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array.
// Alias of Associate().
// Play: https://go.dev/play/p/WHa2CfMO3Lr
func SliceToMap[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V {
return Associate(collection, transform)
}
// Drop drops n elements from the beginning of a slice or array.
// Play: https://go.dev/play/p/JswS7vXRJP2
func Drop[T any](collection []T, n int) []T {
if len(collection) <= n {
return make([]T, 0)
}
result := make([]T, len(collection)-n)
for i := n; i < len(collection); i++ {
result[i-n] = collection[i]
result := make([]T, 0, len(collection)-n)
return append(result, collection[n:]...)
}
// DropRight drops n elements from the end of a slice or array.
// Play: https://go.dev/play/p/GG0nXkSJJa3
func DropRight[T any](collection []T, n int) []T {
if len(collection) <= n {
return []T{}
}
return result
result := make([]T, 0, len(collection)-n)
return append(result, collection[:len(collection)-n]...)
}
// DropWhile drops elements from the beginning of a slice or array while the predicate returns true.
// Play: https://go.dev/play/p/7gBPYw2IK16
func DropWhile[T any](collection []T, predicate func(T) bool) []T {
i := 0
for ; i < len(collection); i++ {
@ -287,30 +359,12 @@ func DropWhile[T any](collection []T, predicate func(T) bool) []T {
}
}
result := make([]T, len(collection)-i)
for j := 0; i < len(collection); i, j = i+1, j+1 {
result[j] = collection[i]
}
return result
}
// DropRight drops n elements from the end of a slice or array.
func DropRight[T any](collection []T, n int) []T {
if len(collection) <= n {
return make([]T, 0)
}
result := make([]T, len(collection)-n)
for i := len(collection) - 1 - n; i >= 0; i-- {
result[i] = collection[i]
}
return result
result := make([]T, 0, len(collection)-i)
return append(result, collection[i:]...)
}
// DropRightWhile drops elements from the end of a slice or array while the predicate returns true.
// Play: https://go.dev/play/p/3-n71oEC0Hz
func DropRightWhile[T any](collection []T, predicate func(T) bool) []T {
i := len(collection) - 1
for ; i >= 0; i-- {
@ -319,16 +373,12 @@ func DropRightWhile[T any](collection []T, predicate func(T) bool) []T {
}
}
result := make([]T, i+1)
for ; i >= 0; i-- {
result[i] = collection[i]
}
return result
result := make([]T, 0, i+1)
return append(result, collection[:i+1]...)
}
// Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for.
// Play: https://go.dev/play/p/YkLMODy1WEL
func Reject[V any](collection []V, predicate func(V, int) bool) []V {
result := []V{}
@ -342,6 +392,7 @@ func Reject[V any](collection []V, predicate func(V, int) bool) []V {
}
// Count counts the number of elements in the collection that compare equal to value.
// Play: https://go.dev/play/p/Y3FlK54yveC
func Count[T comparable](collection []T, value T) (count int) {
for _, item := range collection {
if item == value {
@ -353,6 +404,7 @@ func Count[T comparable](collection []T, value T) (count int) {
}
// CountBy counts the number of elements in the collection for which predicate is true.
// Play: https://go.dev/play/p/ByQbNYQQi4X
func CountBy[T any](collection []T, predicate func(T) bool) (count int) {
for _, item := range collection {
if predicate(item) {
@ -363,7 +415,8 @@ func CountBy[T any](collection []T, predicate func(T) bool) (count int) {
return count
}
// Subset return part of a slice.
// Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow.
// Play: https://go.dev/play/p/tOQu1GhFcog
func Subset[T any](collection []T, offset int, length uint) []T {
size := len(collection)
@ -385,17 +438,36 @@ func Subset[T any](collection []T, offset int, length uint) []T {
return collection[offset : offset+int(length)]
}
// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow.
// Play: https://go.dev/play/p/8XWYhfMMA1h
func Slice[T any](collection []T, start int, end int) []T {
size := len(collection)
if start >= end {
return []T{}
}
if start > size {
start = size
}
if end > size {
end = size
}
return collection[start:end]
}
// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new.
// Play: https://go.dev/play/p/XfPzmf9gql6
func Replace[T comparable](collection []T, old T, new T, n int) []T {
size := len(collection)
result := make([]T, 0, size)
result := make([]T, len(collection))
copy(result, collection)
for _, item := range collection {
if item == old && n != 0 {
result = append(result, new)
for i := range result {
if result[i] == old && n != 0 {
result[i] = new
n--
} else {
result = append(result, item)
}
}
@ -403,6 +475,49 @@ func Replace[T comparable](collection []T, old T, new T, n int) []T {
}
// ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new.
// Play: https://go.dev/play/p/a9xZFUHfYcV
func ReplaceAll[T comparable](collection []T, old T, new T) []T {
return Replace[T](collection, old, new, -1)
return Replace(collection, old, new, -1)
}
// Compact returns a slice of all non-zero elements.
// Play: https://go.dev/play/p/tXiy-iK6PAc
func Compact[T comparable](collection []T) []T {
var zero T
result := []T{}
for _, item := range collection {
if item != zero {
result = append(result, item)
}
}
return result
}
// IsSorted checks if a slice is sorted.
// Play: https://go.dev/play/p/mc3qR-t4mcx
func IsSorted[T constraints.Ordered](collection []T) bool {
for i := 1; i < len(collection); i++ {
if collection[i-1] > collection[i] {
return false
}
}
return true
}
// IsSortedByKey checks if a slice is sorted by iteratee.
// Play: https://go.dev/play/p/wiG6XyBBu49
func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(T) K) bool {
size := len(collection)
for i := 0; i < size-1; i++ {
if iteratee(collection[i]) > iteratee(collection[i+1]) {
return false
}
}
return true
}

@ -1,8 +1,11 @@
package lo
import "unicode/utf8"
import (
"unicode/utf8"
)
// Substring return part of a string.
// Play: https://go.dev/play/p/TQlxQi82Lu1
func Substring[T ~string](str T, offset int, length uint) T {
size := len(str)
@ -24,7 +27,39 @@ func Substring[T ~string](str T, offset int, length uint) T {
return str[offset : offset+int(length)]
}
// ChunkString returns an array of strings split into groups the length of size. If array can't be split evenly,
// the final chunk will be the remaining elements.
// Play: https://go.dev/play/p/__FLTuJVz54
func ChunkString[T ~string](str T, size int) []T {
if size <= 0 {
panic("lo.ChunkString: Size parameter must be greater than 0")
}
if len(str) == 0 {
return []T{""}
}
if size >= len(str) {
return []T{str}
}
var chunks []T = make([]T, 0, ((len(str)-1)/size)+1)
currentLen := 0
currentStart := 0
for i := range str {
if currentLen == size {
chunks = append(chunks, str[currentStart:i])
currentLen = 0
currentStart = i
}
currentLen++
}
chunks = append(chunks, str[currentStart:])
return chunks
}
// RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string.
// Play: https://go.dev/play/p/tuhgW_lWY8l
func RuneLength(str string) int {
return utf8.RuneCountInString(str)
}

@ -0,0 +1,32 @@
package lo
import (
"os"
"testing"
"time"
)
// https://github.com/stretchr/testify/issues/1101
func testWithTimeout(t *testing.T, timeout time.Duration) {
t.Helper()
testFinished := make(chan struct{})
t.Cleanup(func() { close(testFinished) })
go func() {
select {
case <-testFinished:
case <-time.After(timeout):
t.Errorf("test timed out after %s", timeout)
os.Exit(1)
}
}()
}
type foo struct {
bar string
}
func (f foo) Clone() foo {
return foo{f.bar}
}

@ -1,81 +1,97 @@
package lo
// T2 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T2[A any, B any](a A, b B) Tuple2[A, B] {
return Tuple2[A, B]{A: a, B: b}
}
// T3 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T3[A any, B any, C any](a A, b B, c C) Tuple3[A, B, C] {
return Tuple3[A, B, C]{A: a, B: b, C: c}
}
// T4 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T4[A any, B any, C any, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] {
return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d}
}
// T5 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T5[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] {
return Tuple5[A, B, C, D, E]{A: a, B: b, C: c, D: d, E: e}
}
// T6 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T6[A any, B any, C any, D any, E any, F any](a A, b B, c C, d D, e E, f F) Tuple6[A, B, C, D, E, F] {
return Tuple6[A, B, C, D, E, F]{A: a, B: b, C: c, D: d, E: e, F: f}
}
// T7 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T7[A any, B any, C any, D any, E any, F any, G any](a A, b B, c C, d D, e E, f F, g G) Tuple7[A, B, C, D, E, F, G] {
return Tuple7[A, B, C, D, E, F, G]{A: a, B: b, C: c, D: d, E: e, F: f, G: g}
}
// T8 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T8[A any, B any, C any, D any, E any, F any, G any, H any](a A, b B, c C, d D, e E, f F, g G, h H) Tuple8[A, B, C, D, E, F, G, H] {
return Tuple8[A, B, C, D, E, F, G, H]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h}
}
// T8 creates a tuple from a list of values.
// Play: https://go.dev/play/p/IllL3ZO4BQm
func T9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a A, b B, c C, d D, e E, f F, g G, h H, i I) Tuple9[A, B, C, D, E, F, G, H, I] {
return Tuple9[A, B, C, D, E, F, G, H, I]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i}
}
// Unpack2 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) {
return tuple.A, tuple.B
}
// Unpack3 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) {
return tuple.A, tuple.B, tuple.C
}
// Unpack4 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack4[A any, B any, C any, D any](tuple Tuple4[A, B, C, D]) (A, B, C, D) {
return tuple.A, tuple.B, tuple.C, tuple.D
}
// Unpack5 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack5[A any, B any, C any, D any, E any](tuple Tuple5[A, B, C, D, E]) (A, B, C, D, E) {
return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E
}
// Unpack6 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack6[A any, B any, C any, D any, E any, F any](tuple Tuple6[A, B, C, D, E, F]) (A, B, C, D, E, F) {
return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F
}
// Unpack7 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack7[A any, B any, C any, D any, E any, F any, G any](tuple Tuple7[A, B, C, D, E, F, G]) (A, B, C, D, E, F, G) {
return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G
}
// Unpack8 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack8[A any, B any, C any, D any, E any, F any, G any, H any](tuple Tuple8[A, B, C, D, E, F, G, H]) (A, B, C, D, E, F, G, H) {
return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H
}
// Unpack9 returns values contained in tuple.
// Play: https://go.dev/play/p/xVP_k0kJ96W
func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuple Tuple9[A, B, C, D, E, F, G, H, I]) (A, B, C, D, E, F, G, H, I) {
return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H, tuple.I
}
@ -83,14 +99,15 @@ func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tupl
// Zip2 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] {
size := Max[int]([]int{len(a), len(b)})
size := Max([]int{len(a), len(b)})
result := make([]Tuple2[A, B], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
result = append(result, Tuple2[A, B]{
A: _a,
@ -104,15 +121,16 @@ func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] {
// Zip3 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] {
size := Max[int]([]int{len(a), len(b), len(c)})
size := Max([]int{len(a), len(b), len(c)})
result := make([]Tuple3[A, B, C], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
result = append(result, Tuple3[A, B, C]{
A: _a,
@ -127,16 +145,17 @@ func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] {
// Zip4 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] {
size := Max[int]([]int{len(a), len(b), len(c), len(d)})
size := Max([]int{len(a), len(b), len(c), len(d)})
result := make([]Tuple4[A, B, C, D], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
result = append(result, Tuple4[A, B, C, D]{
A: _a,
@ -152,17 +171,18 @@ func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B,
// Zip5 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] {
size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e)})
size := Max([]int{len(a), len(b), len(c), len(d), len(e)})
result := make([]Tuple5[A, B, C, D, E], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_e, _ := Nth[E](e, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
_e, _ := Nth(e, index)
result = append(result, Tuple5[A, B, C, D, E]{
A: _a,
@ -179,18 +199,19 @@ func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E)
// Zip6 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] {
size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f)})
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)})
result := make([]Tuple6[A, B, C, D, E, F], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_e, _ := Nth[E](e, index)
_f, _ := Nth[F](f, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
_e, _ := Nth(e, index)
_f, _ := Nth(f, index)
result = append(result, Tuple6[A, B, C, D, E, F]{
A: _a,
@ -208,19 +229,20 @@ func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D,
// Zip7 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] {
size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)})
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)})
result := make([]Tuple7[A, B, C, D, E, F, G], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_e, _ := Nth[E](e, index)
_f, _ := Nth[F](f, index)
_g, _ := Nth[G](g, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
_e, _ := Nth(e, index)
_f, _ := Nth(f, index)
_g, _ := Nth(g, index)
result = append(result, Tuple7[A, B, C, D, E, F, G]{
A: _a,
@ -239,20 +261,21 @@ func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C,
// Zip8 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] {
size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)})
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)})
result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_e, _ := Nth[E](e, index)
_f, _ := Nth[F](f, index)
_g, _ := Nth[G](g, index)
_h, _ := Nth[H](h, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
_e, _ := Nth(e, index)
_f, _ := Nth(f, index)
_g, _ := Nth(g, index)
_h, _ := Nth(h, index)
result = append(result, Tuple8[A, B, C, D, E, F, G, H]{
A: _a,
@ -272,21 +295,22 @@ func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B,
// Zip9 creates a slice of grouped elements, the first of which contains the first elements
// of the given arrays, the second of which contains the second elements of the given arrays, and so on.
// When collections have different size, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] {
size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)})
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)})
result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size)
for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index)
_b, _ := Nth[B](b, index)
_c, _ := Nth[C](c, index)
_d, _ := Nth[D](d, index)
_e, _ := Nth[E](e, index)
_f, _ := Nth[F](f, index)
_g, _ := Nth[G](g, index)
_h, _ := Nth[H](h, index)
_i, _ := Nth[I](i, index)
_a, _ := Nth(a, index)
_b, _ := Nth(b, index)
_c, _ := Nth(c, index)
_d, _ := Nth(d, index)
_e, _ := Nth(e, index)
_f, _ := Nth(f, index)
_g, _ := Nth(g, index)
_h, _ := Nth(h, index)
_i, _ := Nth(i, index)
result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{
A: _a,
@ -306,6 +330,7 @@ func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A,
// Unzip2 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -321,6 +346,7 @@ func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) {
// Unzip3 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -338,6 +364,7 @@ func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) {
// Unzip4 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -357,6 +384,7 @@ func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B,
// Unzip5 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ([]A, []B, []C, []D, []E) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -378,6 +406,7 @@ func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) (
// Unzip6 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D, E, F]) ([]A, []B, []C, []D, []E, []F) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -401,6 +430,7 @@ func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D
// Unzip7 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, B, C, D, E, F, G]) ([]A, []B, []C, []D, []E, []F, []G) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -426,6 +456,7 @@ func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A,
// Unzip8 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tuple8[A, B, C, D, E, F, G, H]) ([]A, []B, []C, []D, []E, []F, []G, []H) {
size := len(tuples)
r1 := make([]A, 0, size)
@ -453,6 +484,7 @@ func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tup
// Unzip9 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuples []Tuple9[A, B, C, D, E, F, G, H, I]) ([]A, []B, []C, []D, []E, []F, []G, []H, []I) {
size := len(tuples)
r1 := make([]A, 0, size)

@ -0,0 +1,88 @@
package lo
// ToPtr returns a pointer copy of value.
func ToPtr[T any](x T) *T {
return &x
}
// FromPtr returns the pointer value or empty.
func FromPtr[T any](x *T) T {
if x == nil {
return Empty[T]()
}
return *x
}
// FromPtrOr returns the pointer value or the fallback value.
func FromPtrOr[T any](x *T, fallback T) T {
if x == nil {
return fallback
}
return *x
}
// ToSlicePtr returns a slice of pointer copy of value.
func ToSlicePtr[T any](collection []T) []*T {
return Map(collection, func(x T, _ int) *T {
return &x
})
}
// ToAnySlice returns a slice with all elements mapped to `any` type
func ToAnySlice[T any](collection []T) []any {
result := make([]any, len(collection))
for i, item := range collection {
result[i] = item
}
return result
}
// FromAnySlice returns an `any` slice with all elements mapped to a type.
// Returns false in case of type conversion failure.
func FromAnySlice[T any](in []any) (out []T, ok bool) {
defer func() {
if r := recover(); r != nil {
out = []T{}
ok = false
}
}()
result := make([]T, len(in))
for i, item := range in {
result[i] = item.(T)
}
return result, true
}
// Empty returns an empty value.
func Empty[T any]() T {
var zero T
return zero
}
// IsEmpty returns true if argument is a zero value.
func IsEmpty[T comparable](v T) bool {
var zero T
return zero == v
}
// IsNotEmpty returns true if argument is not a zero value.
func IsNotEmpty[T comparable](v T) bool {
var zero T
return zero != v
}
// Coalesce returns the first non-empty arguments. Arguments must be comparable.
func Coalesce[T comparable](v ...T) (result T, ok bool) {
for _, e := range v {
if e != result {
result = e
ok = true
return
}
}
return
}

@ -1,8 +1,10 @@
package assert
import (
"bytes"
"fmt"
"reflect"
"time"
)
type CompareType int
@ -30,6 +32,9 @@ var (
float64Type = reflect.TypeOf(float64(1))
stringType = reflect.TypeOf("")
timeType = reflect.TypeOf(time.Time{})
bytesType = reflect.TypeOf([]byte{})
)
func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
@ -299,6 +304,47 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
return compareLess, true
}
}
// Check for known struct types we can check for compare results.
case reflect.Struct:
{
// All structs enter here. We're not interested in most types.
if !canConvert(obj1Value, timeType) {
break
}
// time.Time can compared!
timeObj1, ok := obj1.(time.Time)
if !ok {
timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time)
}
timeObj2, ok := obj2.(time.Time)
if !ok {
timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time)
}
return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64)
}
case reflect.Slice:
{
// We only care about the []byte type.
if !canConvert(obj1Value, bytesType) {
break
}
// []byte can be compared!
bytesObj1, ok := obj1.([]byte)
if !ok {
bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte)
}
bytesObj2, ok := obj2.([]byte)
if !ok {
bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte)
}
return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true
}
}
return compareEqual, false
@ -310,7 +356,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
// assert.Greater(t, float64(2), float64(1))
// assert.Greater(t, "b", "a")
func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs)
if h, ok := t.(tHelper); ok {
h.Helper()
}
return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...)
}
// GreaterOrEqual asserts that the first element is greater than or equal to the second
@ -320,7 +369,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface
// assert.GreaterOrEqual(t, "b", "a")
// assert.GreaterOrEqual(t, "b", "b")
func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs)
if h, ok := t.(tHelper); ok {
h.Helper()
}
return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...)
}
// Less asserts that the first element is less than the second
@ -329,7 +381,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in
// assert.Less(t, float64(1), float64(2))
// assert.Less(t, "a", "b")
func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs)
if h, ok := t.(tHelper); ok {
h.Helper()
}
return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...)
}
// LessOrEqual asserts that the first element is less than or equal to the second
@ -339,7 +394,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{})
// assert.LessOrEqual(t, "a", "b")
// assert.LessOrEqual(t, "b", "b")
func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {
return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
if h, ok := t.(tHelper); ok {
h.Helper()
}
return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...)
}
// Positive asserts that the specified element is positive
@ -347,8 +405,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter
// assert.Positive(t, 1)
// assert.Positive(t, 1.23)
func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs)
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...)
}
// Negative asserts that the specified element is negative
@ -356,8 +417,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
// assert.Negative(t, -1)
// assert.Negative(t, -1.23)
func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs)
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...)
}
func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {

@ -0,0 +1,16 @@
//go:build go1.17
// +build go1.17
// TODO: once support for Go 1.16 is dropped, this file can be
// merged/removed with assertion_compare_go1.17_test.go and
// assertion_compare_legacy.go
package assert
import "reflect"
// Wrapper around reflect.Value.CanConvert, for compatibility
// reasons.
func canConvert(value reflect.Value, to reflect.Type) bool {
return value.CanConvert(to)
}

@ -0,0 +1,16 @@
//go:build !go1.17
// +build !go1.17
// TODO: once support for Go 1.16 is dropped, this file can be
// merged/removed with assertion_compare_go1.17_test.go and
// assertion_compare_can_convert.go
package assert
import "reflect"
// Older versions of Go does not have the reflect.Value.CanConvert
// method.
func canConvert(value reflect.Value, to reflect.Type) bool {
return false
}

@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int
return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
}
// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
// and that the error contains the specified substring.
//
// actualObj, err := SomeFunction()
// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted")
func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...)
}
// ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
@ -724,6 +736,16 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim
return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...)
}
// YAMLEqf asserts that two YAML strings are equivalent.
func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {

@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ..
return ErrorAsf(a.t, err, target, msg, args...)
}
// ErrorContains asserts that a function returned an error (i.e. not `nil`)
// and that the error contains the specified substring.
//
// actualObj, err := SomeFunction()
// a.ErrorContains(err, expectedErrorSubString)
func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorContains(a.t, theError, contains, msgAndArgs...)
}
// ErrorContainsf asserts that a function returned an error (i.e. not `nil`)
// and that the error contains the specified substring.
//
// actualObj, err := SomeFunction()
// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted")
func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorContainsf(a.t, theError, contains, msg, args...)
}
// ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
@ -1437,6 +1461,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta
return WithinDurationf(a.t, expected, actual, delta, msg, args...)
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return WithinRange(a.t, actual, start, end, msgAndArgs...)
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return WithinRangef(a.t, actual, start, end, msg, args...)
}
// YAMLEq asserts that two YAML strings are equivalent.
func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {

@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT
// assert.IsIncreasing(t, []float{1, 2})
// assert.IsIncreasing(t, []string{"a", "b"})
func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs)
return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...)
}
// IsNonIncreasing asserts that the collection is not increasing
@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo
// assert.IsNonIncreasing(t, []float{2, 1})
// assert.IsNonIncreasing(t, []string{"b", "a"})
func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs)
return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...)
}
// IsDecreasing asserts that the collection is decreasing
@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{})
// assert.IsDecreasing(t, []float{2, 1})
// assert.IsDecreasing(t, []string{"b", "a"})
func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs)
return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...)
}
// IsNonDecreasing asserts that the collection is not decreasing
@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo
// assert.IsNonDecreasing(t, []float{1, 2})
// assert.IsNonDecreasing(t, []string{"a", "b"})
func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...)
}

@ -8,6 +8,7 @@ import (
"fmt"
"math"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
@ -144,7 +145,8 @@ func CallerInfo() []string {
if len(parts) > 1 {
dir := parts[len(parts)-2]
if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" {
callers = append(callers, fmt.Sprintf("%s:%d", file, line))
path, _ := filepath.Abs(file)
callers = append(callers, fmt.Sprintf("%s:%d", path, line))
}
}
@ -563,16 +565,17 @@ func isEmpty(object interface{}) bool {
switch objValue.Kind() {
// collection types are empty when they have no element
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
case reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
// pointers are empty if nil or if the value they point to is empty
// pointers are empty if nil or if the value they point to is empty
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
// for all other types, compare against the zero value
// for all other types, compare against the zero value
// array types are empty when they match their zero-initialized state
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
@ -718,10 +721,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte
// return (false, false) if impossible.
// return (true, false) if element was not found.
// return (true, true) if element was found.
func includeElement(list interface{}, element interface{}) (ok, found bool) {
func containsElement(list interface{}, element interface{}) (ok, found bool) {
listValue := reflect.ValueOf(list)
listKind := reflect.TypeOf(list).Kind()
listType := reflect.TypeOf(list)
if listType == nil {
return false, false
}
listKind := listType.Kind()
defer func() {
if e := recover(); e != nil {
ok = false
@ -764,7 +771,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo
h.Helper()
}
ok, found := includeElement(s, contains)
ok, found := containsElement(s, contains)
if !ok {
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...)
}
@ -787,7 +794,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{})
h.Helper()
}
ok, found := includeElement(s, contains)
ok, found := containsElement(s, contains)
if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...)
}
@ -811,7 +818,6 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
return true // we consider nil to be equal to the nil set
}
subsetValue := reflect.ValueOf(subset)
defer func() {
if e := recover(); e != nil {
ok = false
@ -821,17 +827,35 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).Kind()
if listKind != reflect.Array && listKind != reflect.Slice {
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
}
if subsetKind != reflect.Array && subsetKind != reflect.Slice {
if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
if subsetKind == reflect.Map && listKind == reflect.Map {
listValue := reflect.ValueOf(list)
subsetKeys := subsetValue.MapKeys()
for i := 0; i < len(subsetKeys); i++ {
subsetKey := subsetKeys[i]
subsetElement := subsetValue.MapIndex(subsetKey).Interface()
listElement := listValue.MapIndex(subsetKey).Interface()
if !ObjectsAreEqual(subsetElement, listElement) {
return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...)
}
}
return true
}
for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface()
ok, found := includeElement(list, element)
ok, found := containsElement(list, element)
if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...)
}
@ -852,10 +876,9 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
h.Helper()
}
if subset == nil {
return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...)
return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
defer func() {
if e := recover(); e != nil {
ok = false
@ -865,17 +888,35 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).Kind()
if listKind != reflect.Array && listKind != reflect.Slice {
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
}
if subsetKind != reflect.Array && subsetKind != reflect.Slice {
if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
if subsetKind == reflect.Map && listKind == reflect.Map {
listValue := reflect.ValueOf(list)
subsetKeys := subsetValue.MapKeys()
for i := 0; i < len(subsetKeys); i++ {
subsetKey := subsetKeys[i]
subsetElement := subsetValue.MapIndex(subsetKey).Interface()
listElement := listValue.MapIndex(subsetKey).Interface()
if !ObjectsAreEqual(subsetElement, listElement) {
return true
}
}
return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...)
}
for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface()
ok, found := includeElement(list, element)
ok, found := containsElement(list, element)
if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...)
}
@ -1000,27 +1041,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool {
type PanicTestFunc func()
// didPanic returns true if the function passed to it panics. Otherwise, it returns false.
func didPanic(f PanicTestFunc) (bool, interface{}, string) {
didPanic := false
var message interface{}
var stack string
func() {
defer func() {
if message = recover(); message != nil {
didPanic = true
stack = string(debug.Stack())
}
}()
// call the target function
f()
func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) {
didPanic = true
defer func() {
message = recover()
if didPanic {
stack = string(debug.Stack())
}
}()
return didPanic, message, stack
// call the target function
f()
didPanic = false
return
}
// Panics asserts that the code inside the specified PanicTestFunc panics.
@ -1111,6 +1146,27 @@ func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration,
return true
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if end.Before(start) {
return Fail(t, "Start should be before end", msgAndArgs...)
}
if actual.Before(start) {
return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...)
} else if actual.After(end) {
return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...)
}
return true
}
func toFloat(x interface{}) (float64, bool) {
var xf float64
xok := true
@ -1161,11 +1217,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs
bf, bok := toFloat(actual)
if !aok || !bok {
return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...)
return Fail(t, "Parameters must be numerical", msgAndArgs...)
}
if math.IsNaN(af) && math.IsNaN(bf) {
return true
}
if math.IsNaN(af) {
return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...)
return Fail(t, "Expected must not be NaN", msgAndArgs...)
}
if math.IsNaN(bf) {
@ -1188,7 +1248,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn
if expected == nil || actual == nil ||
reflect.TypeOf(actual).Kind() != reflect.Slice ||
reflect.TypeOf(expected).Kind() != reflect.Slice {
return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
return Fail(t, "Parameters must be slice", msgAndArgs...)
}
actualSlice := reflect.ValueOf(actual)
@ -1250,8 +1310,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m
func calcRelativeError(expected, actual interface{}) (float64, error) {
af, aok := toFloat(expected)
if !aok {
return 0, fmt.Errorf("expected value %q cannot be converted to float", expected)
bf, bok := toFloat(actual)
if !aok || !bok {
return 0, fmt.Errorf("Parameters must be numerical")
}
if math.IsNaN(af) && math.IsNaN(bf) {
return 0, nil
}
if math.IsNaN(af) {
return 0, errors.New("expected value must not be NaN")
@ -1259,10 +1323,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) {
if af == 0 {
return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error")
}
bf, bok := toFloat(actual)
if !bok {
return 0, fmt.Errorf("actual value %q cannot be converted to float", actual)
}
if math.IsNaN(bf) {
return 0, errors.New("actual value must not be NaN")
}
@ -1298,7 +1358,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m
if expected == nil || actual == nil ||
reflect.TypeOf(actual).Kind() != reflect.Slice ||
reflect.TypeOf(expected).Kind() != reflect.Slice {
return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
return Fail(t, "Parameters must be slice", msgAndArgs...)
}
actualSlice := reflect.ValueOf(actual)
@ -1375,6 +1435,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte
return true
}
// ErrorContains asserts that a function returned an error (i.e. not `nil`)
// and that the error contains the specified substring.
//
// actualObj, err := SomeFunction()
// assert.ErrorContains(t, err, expectedErrorSubString)
func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if !Error(t, theError, msgAndArgs...) {
return false
}
actual := theError.Error()
if !strings.Contains(actual, contains) {
return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...)
}
return true
}
// matchRegexp return true if a specified regexp matches a string.
func matchRegexp(rx interface{}, str interface{}) bool {
@ -1588,12 +1669,17 @@ func diff(expected interface{}, actual interface{}) string {
}
var e, a string
if et != reflect.TypeOf("") {
e = spewConfig.Sdump(expected)
a = spewConfig.Sdump(actual)
} else {
switch et {
case reflect.TypeOf(""):
e = reflect.ValueOf(expected).String()
a = reflect.ValueOf(actual).String()
case reflect.TypeOf(time.Time{}):
e = spewConfigStringerEnabled.Sdump(expected)
a = spewConfigStringerEnabled.Sdump(actual)
default:
e = spewConfig.Sdump(expected)
a = spewConfig.Sdump(actual)
}
diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
@ -1625,6 +1711,14 @@ var spewConfig = spew.ConfigState{
MaxDepth: 10,
}
var spewConfigStringerEnabled = spew.ConfigState{
Indent: " ",
DisablePointerAddresses: true,
DisableCapacities: true,
SortKeys: true,
MaxDepth: 10,
}
type tHelper interface {
Helper()
}

78
vendor/gopkg.in/yaml.v3/decode.go generated vendored

@ -100,7 +100,10 @@ func (p *parser) peek() yaml_event_type_t {
if p.event.typ != yaml_NO_EVENT {
return p.event.typ
}
if !yaml_parser_parse(&p.parser, &p.event) {
// It's curious choice from the underlying API to generally return a
// positive result on success, but on this case return true in an error
// scenario. This was the source of bugs in the past (issue #666).
if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR {
p.fail()
}
return p.event.typ
@ -320,6 +323,8 @@ type decoder struct {
decodeCount int
aliasCount int
aliasDepth int
mergedFields map[interface{}]bool
}
var (
@ -808,6 +813,11 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
}
}
mergedFields := d.mergedFields
d.mergedFields = nil
var mergeNode *Node
mapIsNew := false
if out.IsNil() {
out.Set(reflect.MakeMap(outt))
@ -815,11 +825,18 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
}
for i := 0; i < l; i += 2 {
if isMerge(n.Content[i]) {
d.merge(n.Content[i+1], out)
mergeNode = n.Content[i+1]
continue
}
k := reflect.New(kt).Elem()
if d.unmarshal(n.Content[i], k) {
if mergedFields != nil {
ki := k.Interface()
if mergedFields[ki] {
continue
}
mergedFields[ki] = true
}
kkind := k.Kind()
if kkind == reflect.Interface {
kkind = k.Elem().Kind()
@ -833,6 +850,12 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
}
}
}
d.mergedFields = mergedFields
if mergeNode != nil {
d.merge(n, mergeNode, out)
}
d.stringMapType = stringMapType
d.generalMapType = generalMapType
return true
@ -844,7 +867,8 @@ func isStringMap(n *Node) bool {
}
l := len(n.Content)
for i := 0; i < l; i += 2 {
if n.Content[i].ShortTag() != strTag {
shortTag := n.Content[i].ShortTag()
if shortTag != strTag && shortTag != mergeTag {
return false
}
}
@ -861,7 +885,6 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
var elemType reflect.Type
if sinfo.InlineMap != -1 {
inlineMap = out.Field(sinfo.InlineMap)
inlineMap.Set(reflect.New(inlineMap.Type()).Elem())
elemType = inlineMap.Type().Elem()
}
@ -870,6 +893,9 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
d.prepare(n, field)
}
mergedFields := d.mergedFields
d.mergedFields = nil
var mergeNode *Node
var doneFields []bool
if d.uniqueKeys {
doneFields = make([]bool, len(sinfo.FieldsList))
@ -879,13 +905,20 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
for i := 0; i < l; i += 2 {
ni := n.Content[i]
if isMerge(ni) {
d.merge(n.Content[i+1], out)
mergeNode = n.Content[i+1]
continue
}
if !d.unmarshal(ni, name) {
continue
}
if info, ok := sinfo.FieldsMap[name.String()]; ok {
sname := name.String()
if mergedFields != nil {
if mergedFields[sname] {
continue
}
mergedFields[sname] = true
}
if info, ok := sinfo.FieldsMap[sname]; ok {
if d.uniqueKeys {
if doneFields[info.Id] {
d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type()))
@ -911,6 +944,11 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type()))
}
}
d.mergedFields = mergedFields
if mergeNode != nil {
d.merge(n, mergeNode, out)
}
return true
}
@ -918,19 +956,29 @@ func failWantMap() {
failf("map merge requires map or sequence of maps as the value")
}
func (d *decoder) merge(n *Node, out reflect.Value) {
switch n.Kind {
func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) {
mergedFields := d.mergedFields
if mergedFields == nil {
d.mergedFields = make(map[interface{}]bool)
for i := 0; i < len(parent.Content); i += 2 {
k := reflect.New(ifaceType).Elem()
if d.unmarshal(parent.Content[i], k) {
d.mergedFields[k.Interface()] = true
}
}
}
switch merge.Kind {
case MappingNode:
d.unmarshal(n, out)
d.unmarshal(merge, out)
case AliasNode:
if n.Alias != nil && n.Alias.Kind != MappingNode {
if merge.Alias != nil && merge.Alias.Kind != MappingNode {
failWantMap()
}
d.unmarshal(n, out)
d.unmarshal(merge, out)
case SequenceNode:
// Step backwards as earlier nodes take precedence.
for i := len(n.Content) - 1; i >= 0; i-- {
ni := n.Content[i]
for i := 0; i < len(merge.Content); i++ {
ni := merge.Content[i]
if ni.Kind == AliasNode {
if ni.Alias != nil && ni.Alias.Kind != MappingNode {
failWantMap()
@ -943,6 +991,8 @@ func (d *decoder) merge(n *Node, out reflect.Value) {
default:
failWantMap()
}
d.mergedFields = mergedFields
}
func isMerge(n *Node) bool {

@ -687,6 +687,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i
func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark)
skip_token(parser)
}
@ -786,7 +789,7 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
}
token := peek_token(parser)
if token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN {
if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN {
return
}
@ -813,6 +816,9 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark)
skip_token(parser)
}
@ -922,6 +928,9 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev
func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first {
token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark)
skip_token(parser)
}

10
vendor/modules.txt vendored

@ -121,6 +121,10 @@ github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
## explicit; go 1.18
github.com/jesseduffield/kill
# github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316
## explicit; go 1.18
github.com/jesseduffield/lazycore/pkg/boxlayout
github.com/jesseduffield/lazycore/pkg/utils
# github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
## explicit
github.com/jesseduffield/yaml
@ -169,7 +173,7 @@ github.com/pmezard/go-difflib/difflib
# github.com/rivo/uniseg v0.2.0
## explicit; go 1.12
github.com/rivo/uniseg
# github.com/samber/lo v1.20.0
# github.com/samber/lo v1.31.0
## explicit; go 1.18
github.com/samber/lo
# github.com/sirupsen/logrus v1.4.2
@ -178,7 +182,7 @@ github.com/sirupsen/logrus
# github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
## explicit
github.com/spkg/bom
# github.com/stretchr/testify v1.7.0
# github.com/stretchr/testify v1.8.0
## explicit; go 1.13
github.com/stretchr/testify/assert
# github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
@ -213,7 +217,7 @@ golang.org/x/xerrors
golang.org/x/xerrors/internal
# gopkg.in/yaml.v2 v2.2.2
## explicit
# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
# gopkg.in/yaml.v3 v3.0.1
## explicit
gopkg.in/yaml.v3
# gotest.tools/v3 v3.2.0

Loading…
Cancel
Save