diff --git a/server/internal/v2/conn.go b/server/internal/v2/conn.go index 4c7632e..8ef69ef 100644 --- a/server/internal/v2/conn.go +++ b/server/internal/v2/conn.go @@ -9,14 +9,14 @@ import ( type ConnWriter func(packet Packet) error type Conn interface { - // SetActor 设置连接使用的 Actor 名称 - SetActor(actor string) + // SetQueue 设置连接使用的消息队列名称 + SetQueue(queue string) - // DelActor 删除连接使用的 Actor - DelActor() + // DelQueue 删除连接使用的消息队列,删除后连接将在系统队列执行消息 + DelQueue() - // GetActor 获取连接使用的 Actor 名称 - GetActor() string + // GetQueue 获取连接使用的消息队列名称 + GetQueue() string // WritePacket 写入一个 Packet WritePacket(packet Packet) error @@ -43,19 +43,19 @@ type conn struct { server *server conn net.Conn // 连接 writer ConnWriter // 写入器 - actor atomic.Pointer[string] // Actor 名称 + queue atomic.Pointer[string] // Actor 名称 } -func (c *conn) SetActor(actor string) { - c.actor.Store(&actor) +func (c *conn) SetQueue(queue string) { + c.queue.Store(&queue) } -func (c *conn) DelActor() { - c.actor.Store(nil) +func (c *conn) DelQueue() { + c.queue.Store(nil) } -func (c *conn) GetActor() string { - ident := c.actor.Load() +func (c *conn) GetQueue() string { + ident := c.queue.Load() if ident == nil { return "" } diff --git a/server/internal/v2/controller.go b/server/internal/v2/controller.go index 36dd680..5fd9abd 100644 --- a/server/internal/v2/controller.go +++ b/server/internal/v2/controller.go @@ -40,7 +40,7 @@ func (s *controller) GetAnts() *ants.Pool { } func (s *controller) RegisterConnection(conn net.Conn, writer ConnWriter) { - s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { c := newConn(s.server, conn, writer) s.server.connections[conn] = c s.events.onConnectionOpened(c) @@ -48,7 +48,7 @@ func (s *controller) RegisterConnection(conn net.Conn, writer ConnWriter) { } func (s *controller) EliminateConnection(conn net.Conn, err error) { - s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { c, exist := s.server.connections[conn] if !exist { return @@ -59,12 +59,12 @@ func (s *controller) EliminateConnection(conn net.Conn, err error) { } func (s *controller) ReactPacket(conn net.Conn, packet Packet) { - s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.server.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { c, exist := s.server.connections[conn] if !exist { return } - s.PublishSyncMessage(c.GetActor(), func(ctx context.Context, srv Server) { + s.PublishSyncMessage(c.GetQueue(), func(ctx context.Context) { s.events.onConnectionReceivePacket(c, packet) }) }) diff --git a/server/internal/v2/events.go b/server/internal/v2/events.go index 3711fd4..b30dd54 100644 --- a/server/internal/v2/events.go +++ b/server/internal/v2/events.go @@ -3,7 +3,6 @@ package server import ( "context" "fmt" - "github.com/kercylan98/minotaur/server/internal/v2/message/messages" "github.com/kercylan98/minotaur/utils/log" "reflect" "time" @@ -68,17 +67,12 @@ func (s *events) onLaunched() { opt.logger.Info("Minotaur Server", log.String("", "============================================================================")) }) - s.PublishMessage(messages.Synchronous( - Server(s.server), - newProducer(s.server, nil), - s.server.getSysQueue(), - func(ctx context.Context, broker Server) { - s.launchedEventHandlers.RangeValue(func(index int, value LaunchedEventHandler) bool { - value(s.server, s.server.state.Ip, s.server.state.LaunchedAt) - return true - }) - }, - )) + s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { + s.launchedEventHandlers.RangeValue(func(index int, value LaunchedEventHandler) bool { + value(s.server, s.server.state.Ip, s.server.state.LaunchedAt) + return true + }) + }) } func (s *events) RegisterConnectionOpenedEvent(handler ConnectionOpenedEventHandler, priority ...int) { @@ -86,9 +80,9 @@ func (s *events) RegisterConnectionOpenedEvent(handler ConnectionOpenedEventHand } func (s *events) onConnectionOpened(conn Conn) { - s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { s.connectionOpenedEventHandlers.RangeValue(func(index int, value ConnectionOpenedEventHandler) bool { - value(srv, conn) + value(s, conn) return true }) }) @@ -99,9 +93,9 @@ func (s *events) RegisterConnectionClosedEvent(handler ConnectionClosedEventHand } func (s *events) onConnectionClosed(conn Conn, err error) { - s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { s.connectionClosedEventHandlers.RangeValue(func(index int, value ConnectionClosedEventHandler) bool { - value(srv, conn, err) + value(s, conn, err) return true }) }) @@ -112,9 +106,9 @@ func (s *events) RegisterConnectionReceivePacketEvent(handler ConnectionReceiveP } func (s *events) onConnectionReceivePacket(conn *conn, packet Packet) { - s.PublishSyncMessage(conn.GetActor(), func(ctx context.Context, srv Server) { + s.PublishSyncMessage(conn.GetQueue(), func(ctx context.Context) { s.connectionReceivePacketEventHandlers.RangeValue(func(index int, value ConnectionReceivePacketEventHandler) bool { - value(srv, conn, packet) + value(s, conn, packet) return true }) }) @@ -125,9 +119,9 @@ func (s *events) RegisterShutdownEvent(handler ShutdownEventHandler, priority .. } func (s *events) onShutdown() { - s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context, srv Server) { + s.PublishSyncMessage(s.getSysQueue(), func(ctx context.Context) { s.shutdownEventHandlers.RangeValue(func(index int, value ShutdownEventHandler) bool { - value(srv) + value(s) return true }) }) diff --git a/server/internal/v2/message/broker.go b/server/internal/v2/message/broker.go deleted file mode 100644 index 027ac89..0000000 --- a/server/internal/v2/message/broker.go +++ /dev/null @@ -1,6 +0,0 @@ -package message - -// Broker 消息核心的接口定义 -type Broker[P Producer, Q Queue] interface { - PublishMessage(message Message[P, Q]) -} diff --git a/server/internal/v2/message/message.go b/server/internal/v2/message/message.go deleted file mode 100644 index 39d3485..0000000 --- a/server/internal/v2/message/message.go +++ /dev/null @@ -1,16 +0,0 @@ -package message - -import ( - "context" - "github.com/kercylan98/minotaur/server/internal/v2/queue" -) - -type Message[P Producer, Q Queue] interface { - OnInitialize(ctx context.Context) - OnProcess() - - // GetProducer 获取消息生产者 - GetProducer() P - - queue.Message[Q] -} diff --git a/server/internal/v2/message/messages/asynchronous.go b/server/internal/v2/message/messages/asynchronous.go deleted file mode 100644 index 4ca4275..0000000 --- a/server/internal/v2/message/messages/asynchronous.go +++ /dev/null @@ -1,92 +0,0 @@ -package messages - -import ( - "context" - "github.com/kercylan98/minotaur/server/internal/v2/message" - "github.com/kercylan98/minotaur/server/internal/v2/queue" -) - -type ( - AsynchronousActuator[P message.Producer, Q message.Queue, B message.Broker[P, Q]] func(context.Context, B, func(context.Context, B)) // 负责执行异步消息的执行器 - AsynchronousHandler[P message.Producer, Q message.Queue, B message.Broker[P, Q]] func(context.Context, B) error // 异步消息逻辑处理器 - AsynchronousCallbackHandler[P message.Producer, Q message.Queue, B message.Broker[P, Q]] func(context.Context, B, error) // 异步消息回调处理器 -) - -// Asynchronous 创建一个异步消息实例,并指定相应的处理器。 -// 该函数接收以下参数: -// - broker:消息所属的 Broker 实例。 -// - actuator:异步消息的执行器,负责执行异步消息的逻辑,当该参数为空时,将会使用默认的 go func()。 -// - handler:异步消息的逻辑处理器,用于执行实际的异步消息处理逻辑,可选参数。 -// - callback:异步消息的回调处理器,处理消息处理完成后的回调操作,可选参数。 -// - afterHandler:异步消息执行完成后的处理器,用于进行后续的处理操作,可选参数。 -// -// 该函数除了 handler,其他所有处理器均为同步执行 -// -// 返回值为一个实现了 Message 接口的异步消息实例。 -func Asynchronous[P message.Producer, Q message.Queue, B message.Broker[P, Q]]( - broker B, producer P, queue Q, - actuator AsynchronousActuator[P, Q, B], - handler AsynchronousHandler[P, Q, B], - callback AsynchronousCallbackHandler[P, Q, B], -) message.Message[P, Q] { - m := &asynchronous[P, Q, B]{ - broker: broker, - producer: producer, - queue: queue, - actuator: actuator, - handler: handler, - callback: callback, - } - if m.actuator == nil { - m.actuator = func(ctx context.Context, b B, f func(context.Context, B)) { - go f(ctx, b) - } - } - - return m -} - -type asynchronous[P message.Producer, Q message.Queue, B message.Broker[P, Q]] struct { - broker B - producer P - queue Q - ctx context.Context - actuator AsynchronousActuator[P, Q, B] - handler AsynchronousHandler[P, Q, B] - callback AsynchronousCallbackHandler[P, Q, B] -} - -func (s *asynchronous[P, Q, B]) OnPublished(controller queue.Controller) { - controller.IncrementCustomMessageCount(1) -} - -func (s *asynchronous[P, Q, B]) OnProcessed(controller queue.Controller) { - controller.IncrementCustomMessageCount(-1) -} - -func (s *asynchronous[P, Q, B]) OnInitialize(ctx context.Context) { - s.ctx = ctx -} - -func (s *asynchronous[P, Q, B]) OnProcess() { - s.actuator(s.ctx, s.broker, func(ctx context.Context, broker B) { - var err error - if s.handler != nil { - err = s.handler(s.ctx, s.broker) - } - - broker.PublishMessage(Synchronous(broker, s.producer, s.queue, func(ctx context.Context, broker B) { - if s.callback != nil { - s.callback(ctx, broker, err) - } - })) - }) -} - -func (s *asynchronous[P, Q, B]) GetProducer() P { - return s.producer -} - -func (s *asynchronous[P, Q, B]) GetQueue() Q { - return s.queue -} diff --git a/server/internal/v2/message/messages/synchronous.go b/server/internal/v2/message/messages/synchronous.go deleted file mode 100644 index bb32677..0000000 --- a/server/internal/v2/message/messages/synchronous.go +++ /dev/null @@ -1,55 +0,0 @@ -package messages - -import ( - "context" - "github.com/kercylan98/minotaur/server/internal/v2/message" - "github.com/kercylan98/minotaur/server/internal/v2/queue" -) - -type ( - SynchronousHandler[P message.Producer, Q message.Queue, B message.Broker[P, Q]] func(context.Context, B) -) - -func Synchronous[P message.Producer, Q message.Queue, B message.Broker[P, Q]]( - broker B, producer P, queue Q, - handler SynchronousHandler[P, Q, B], -) message.Message[P, Q] { - return &synchronous[P, Q, B]{ - broker: broker, - producer: producer, - queue: queue, - handler: handler, - } -} - -type synchronous[P message.Producer, Q message.Queue, B message.Broker[P, Q]] struct { - broker B - producer P - queue Q - ctx context.Context - handler SynchronousHandler[P, Q, B] -} - -func (s *synchronous[P, Q, B]) OnPublished(controller queue.Controller) { - -} - -func (s *synchronous[P, Q, B]) OnProcessed(controller queue.Controller) { - -} - -func (s *synchronous[P, Q, B]) OnInitialize(ctx context.Context) { - s.ctx = ctx -} - -func (s *synchronous[P, Q, B]) OnProcess() { - s.handler(s.ctx, s.broker) -} - -func (s *synchronous[P, Q, B]) GetProducer() P { - return s.producer -} - -func (s *synchronous[P, Q, B]) GetQueue() Q { - return s.queue -} diff --git a/server/internal/v2/message/producer.go b/server/internal/v2/message/producer.go deleted file mode 100644 index 0691634..0000000 --- a/server/internal/v2/message/producer.go +++ /dev/null @@ -1,5 +0,0 @@ -package message - -type Producer interface { - -} diff --git a/server/internal/v2/message/queue.go b/server/internal/v2/message/queue.go deleted file mode 100644 index c3c0233..0000000 --- a/server/internal/v2/message/queue.go +++ /dev/null @@ -1,3 +0,0 @@ -package message - -type Queue comparable diff --git a/server/internal/v2/options.go b/server/internal/v2/options.go index 8d282c8..9ef9562 100644 --- a/server/internal/v2/options.go +++ b/server/internal/v2/options.go @@ -1,7 +1,6 @@ package server import ( - "github.com/kercylan98/minotaur/server/internal/v2/message" "github.com/kercylan98/minotaur/utils/log/v2" "os" "sync" @@ -18,7 +17,6 @@ func DefaultOptions() *Options { actorMessageChannelSize: 1024, serverMessageBufferInitialSize: 1024, actorMessageBufferInitialSize: 1024, - messageErrorHandler: nil, lifeCycleLimit: 0, logger: log.NewLogger(log.NewHandler(os.Stdout, log.DefaultOptions().WithCallerSkip(-1).WithLevel(log.LevelInfo))), } @@ -27,16 +25,15 @@ func DefaultOptions() *Options { type Options struct { server *server rw sync.RWMutex - serverMessageChannelSize int // 服务器 Actor 消息处理管道大小 - actorMessageChannelSize int // Actor 消息处理管道大小 - serverMessageBufferInitialSize int // 服务器 Actor 消息写入缓冲区初始化大小 - actorMessageBufferInitialSize int // Actor 消息写入缓冲区初始化大小 - messageErrorHandler func(srv Server, message message.Message[Producer, string], err error) // 消息错误处理器 - lifeCycleLimit time.Duration // 服务器生命周期上限,在服务器启动后达到生命周期上限将关闭服务器 - logger *log.Logger // 日志记录器 - debug bool // Debug 模式 - syncLowMessageDuration time.Duration // 同步慢消息时间 - asyncLowMessageDuration time.Duration // 异步慢消息时间 + serverMessageChannelSize int // 服务器 Actor 消息处理管道大小 + actorMessageChannelSize int // Actor 消息处理管道大小 + serverMessageBufferInitialSize int // 服务器 Actor 消息写入缓冲区初始化大小 + actorMessageBufferInitialSize int // Actor 消息写入缓冲区初始化大小 + lifeCycleLimit time.Duration // 服务器生命周期上限,在服务器启动后达到生命周期上限将关闭服务器 + logger *log.Logger // 日志记录器 + debug bool // Debug 模式 + syncLowMessageDuration time.Duration // 同步慢消息时间 + asyncLowMessageDuration time.Duration // 异步慢消息时间 } func (opt *Options) init(srv *server) *Options { @@ -54,7 +51,6 @@ func (opt *Options) Apply(options ...*Options) { opt.actorMessageChannelSize = option.actorMessageChannelSize opt.serverMessageBufferInitialSize = option.serverMessageBufferInitialSize opt.actorMessageBufferInitialSize = option.actorMessageBufferInitialSize - opt.messageErrorHandler = option.messageErrorHandler opt.lifeCycleLimit = option.lifeCycleLimit opt.logger = option.logger opt.debug = option.debug @@ -65,9 +61,6 @@ func (opt *Options) Apply(options ...*Options) { } if opt.server != nil && !opt.server.state.LaunchedAt.IsZero() { opt.active() - if opt.server.reactor != nil { - opt.server.reactor.SetLogger(opt.logger) - } } } @@ -121,9 +114,6 @@ func (opt *Options) IsDebug() bool { func (opt *Options) WithLogger(logger *log.Logger) *Options { return opt.modifyOptionsValue(func(opt *Options) { opt.logger = logger - if opt.server != nil && opt.server.reactor != nil { - opt.server.reactor.SetLogger(opt.logger) - } }) } @@ -188,20 +178,6 @@ func (opt *Options) GetActorMessageBufferInitialSize() int { }) } -// WithMessageErrorHandler 设置消息错误处理器,当消息处理出现错误时,会调用该处理器进行处理 -// - 如果在运行时设置,后续消息错误将会使用新的 handler 进行处理 -func (opt *Options) WithMessageErrorHandler(handler func(srv Server, message message.Message[Producer, string], err error)) *Options { - return opt.modifyOptionsValue(func(opt *Options) { - opt.messageErrorHandler = handler - }) -} - -func (opt *Options) GetMessageErrorHandler() func(srv Server, message message.Message[Producer, string], err error) { - return getOptionsValue(opt, func(opt *Options) func(srv Server, message message.Message[Producer, string], err error) { - return opt.messageErrorHandler - }) -} - // WithLifeCycleLimit 设置服务器生命周期上限,在服务器启动后达到生命周期上限将关闭服务器 // - 如果设置为 <= 0 的值,将不限制服务器生命周期 // - 该函数支持运行时设置 diff --git a/server/internal/v2/queue/controller.go b/server/internal/v2/queue/controller.go deleted file mode 100644 index 37dc5ff..0000000 --- a/server/internal/v2/queue/controller.go +++ /dev/null @@ -1,34 +0,0 @@ -package queue - -type ( - incrementCustomMessageCountHandler func(delta int64) -) - -func newController[Id, Q comparable, M Message[Q]](queue *Queue[Id, Q, M], message Message[Q]) Controller { - return Controller{ - incrementCustomMessageCount: func(delta int64) { - queueName := message.GetQueue() - - queue.cond.L.Lock() - - currIdent := queue.identifiers[queueName] - currIdent += delta - queue.identifiers[queueName] = currIdent - queue.state.total += delta - //log.Info("消息总计数", log.Int64("计数", q.state.total)) - - queue.cond.Signal() - queue.cond.L.Unlock() - }, - } -} - -// Controller 队列控制器 -type Controller struct { - incrementCustomMessageCount incrementCustomMessageCountHandler -} - -// IncrementCustomMessageCount 增加自定义消息计数,当消息计数不为 > 0 时会导致队列关闭进入等待状态 -func (c Controller) IncrementCustomMessageCount(delta int64) { - c.incrementCustomMessageCount(delta) -} diff --git a/server/internal/v2/queue/errors.go b/server/internal/v2/queue/errors.go deleted file mode 100644 index 9675217..0000000 --- a/server/internal/v2/queue/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package queue - -import ( - "errors" -) - -var ( - ErrorQueueClosed = errors.New("queue closed") // 队列已关闭 - ErrorQueueInvalid = errors.New("queue invalid") // 无效的队列 -) diff --git a/server/internal/v2/queue/message.go b/server/internal/v2/queue/message.go deleted file mode 100644 index 3bea6f2..0000000 --- a/server/internal/v2/queue/message.go +++ /dev/null @@ -1,11 +0,0 @@ -package queue - -// Message 消息接口定义 -type Message[Queue comparable] interface { - // GetQueue 获取消息执行队列 - GetQueue() Queue - // OnPublished 消息发布成功 - OnPublished(controller Controller) - // OnProcessed 消息处理完成 - OnProcessed(controller Controller) -} diff --git a/server/internal/v2/queue/message_handler.go b/server/internal/v2/queue/message_handler.go deleted file mode 100644 index 011ea39..0000000 --- a/server/internal/v2/queue/message_handler.go +++ /dev/null @@ -1,9 +0,0 @@ -package queue - -// MessageHandler 消息处理器支持传入两个函数对消息进行处理 -// - 在 handler 内可以执行对消息的逻辑 -// - 在 finisher 函数中可以接收到该消息是否是最后一条消息 -type MessageHandler[Id, Q comparable, M Message[Q]] func( - handler func(m M), - finisher func(m M, last bool), -) diff --git a/server/internal/v2/queue/queue.go b/server/internal/v2/queue/queue.go deleted file mode 100644 index 2ab5ae8..0000000 --- a/server/internal/v2/queue/queue.go +++ /dev/null @@ -1,174 +0,0 @@ -package queue - -import ( - "github.com/kercylan98/minotaur/utils/buffer" - "sync" - "sync/atomic" -) - -// New 创建一个并发安全的队列 Queue,该队列支持通过 chanSize 自定义读取 channel 的大小,同支持使用 bufferSize 指定 buffer.Ring 的初始大小 -func New[Id, Q comparable, M Message[Q]](id Id, chanSize, bufferSize int) *Queue[Id, Q, M] { - q := &Queue[Id, Q, M]{ - c: make(chan MessageHandler[Id, Q, M], chanSize), - buf: buffer.NewRing[wrapper[Id, Q, M]](bufferSize), - condRW: &sync.RWMutex{}, - identifiers: make(map[Q]int64), - } - q.cond = sync.NewCond(q.condRW) - q.state = &State[Id, Q, M]{ - queue: q, - id: id, - status: StatusNone, - } - return q -} - -// Queue 队列是一个适用于消息处理等场景的并发安全的数据结构 -// - 该队列接收自定义的消息 M,并将消息有序的传入 Read 函数所返回的 channel 中以供处理 -// - 该结构主要实现目标为读写分离且并发安全的非阻塞传输队列,当消费阻塞时以牺牲内存为代价换取消息的生产不阻塞,适用于服务器消息处理等 -// - 该队列保证了消息的完整性,确保消息不丢失,在队列关闭后会等待所有消息处理完毕后进行关闭,并提供 SetClosedHandler 函数来监听队列的关闭信号 -type Queue[Id, Q comparable, M Message[Q]] struct { - state *State[Id, Q, M] // 队列状态信息 - c chan MessageHandler[Id, Q, M] // 消息读取通道 - buf *buffer.Ring[wrapper[Id, Q, M]] // 消息缓冲区 - cond *sync.Cond // 条件变量 - condRW *sync.RWMutex // 条件变量的读写锁 - closedHandler func(q *Queue[Id, Q, M]) // 关闭处理函数 - identifiers map[Q]int64 // 标识符在队列的消息计数映射 -} - -// Id 获取队列 Id -func (q *Queue[Id, Q, M]) Id() Id { - return q.state.id -} - -// SetId 设置队列 Id -func (q *Queue[Id, Q, M]) SetId(id Id) { - q.state.id = id -} - -// SetClosedHandler 设置队列关闭处理函数,在队列关闭后将执行该函数。此刻队列不再可用 -// - Close 函数为非阻塞调用,调用后不会立即关闭队列,会等待消息处理完毕且处理期间不再有新消息介入 -func (q *Queue[Id, Q, M]) SetClosedHandler(handler func(q *Queue[Id, Q, M])) { - q.closedHandler = handler -} - -// Run 阻塞的运行队列,当队列非首次运行时,将会引发来自 ErrorQueueInvalid 的 panic -func (q *Queue[Id, Q, M]) Run() { - if atomic.LoadInt32(&q.state.status) != StatusNone { - panic(ErrorQueueInvalid) - } - atomic.StoreInt32(&q.state.status, StatusRunning) - defer func(q *Queue[Id, Q, M]) { - if q.closedHandler != nil { - q.closedHandler(q) - } - }(q) - for { - q.cond.L.Lock() - for q.buf.IsEmpty() { - if atomic.LoadInt32(&q.state.status) >= StatusClosing && q.state.total == 0 { - q.cond.L.Unlock() - atomic.StoreInt32(&q.state.status, StatusClosed) - close(q.c) - return - } - q.cond.Wait() - } - items := q.buf.ReadAll() - q.cond.L.Unlock() - for i := 0; i < len(items); i++ { - item := items[i] - q.c <- func(handler func(m M), finisher func(m M, last bool)) { - defer func(msg M) { - msg.OnProcessed(item.controller) - - queue := msg.GetQueue() - - q.cond.L.Lock() - q.state.total-- - curr := q.identifiers[queue] - 1 - if curr != 0 { - q.identifiers[queue] = curr - } else { - delete(q.identifiers, queue) - } - if finisher != nil { - finisher(msg, curr == 0) - } - //log.Info("消息总计数", log.Int64("计数", q.state.total)) - q.cond.Signal() - q.cond.L.Unlock() - }(item.message) - - handler(item.message) - } - } - } -} - -// Push 向队列中推送来自 queue 的消息 m,当队列已关闭时将会返回 ErrorQueueClosed -func (q *Queue[Id, Q, M]) Push(queue Q, m M) error { - if atomic.LoadInt32(&q.state.status) > StatusClosing { - return ErrorQueueClosed - } - wrapper := newWrapper(q, m) - - q.cond.L.Lock() - q.identifiers[queue]++ - q.state.total++ - q.buf.Write(wrapper) - //log.Info("消息总计数", log.Int64("计数", q.state.total)) - q.cond.Signal() - q.cond.L.Unlock() - - m.OnPublished(wrapper.controller) - return nil -} - -// WaitAdd 向队列增加来自外部的等待计数,在队列关闭时会等待该计数归零 -func (q *Queue[Id, Q, M]) WaitAdd(queue Q, delta int64) { - q.cond.L.Lock() - - currIdent := q.identifiers[queue] - currIdent += delta - q.identifiers[queue] = currIdent - q.state.total += delta - //log.Info("消息总计数", log.Int64("计数", q.state.total)) - - q.cond.Signal() - q.cond.L.Unlock() -} - -// Read 获取队列消息的只读通道, -func (q *Queue[Id, Q, M]) Read() <-chan MessageHandler[Id, Q, M] { - return q.c -} - -// Close 关闭队列 -func (q *Queue[Id, Q, M]) Close() { - atomic.CompareAndSwapInt32(&q.state.status, StatusRunning, StatusClosing) - q.cond.Broadcast() -} - -// State 获取队列状态 -func (q *Queue[Id, Q, M]) State() *State[Id, Q, M] { - return q.state -} - -// GetMessageCount 获取消息数量 -func (q *Queue[Id, Q, M]) GetMessageCount() (count int64) { - q.condRW.RLock() - defer q.condRW.RUnlock() - for _, i := range q.identifiers { - count += i - } - return -} - -// GetMessageCountWithIdent 获取特定消息人的消息数量 -func (q *Queue[Id, Q, M]) GetMessageCountWithIdent(queue Q) int64 { - q.condRW.RLock() - defer q.condRW.RUnlock() - return q.identifiers[queue] -} diff --git a/server/internal/v2/queue/state.go b/server/internal/v2/queue/state.go deleted file mode 100644 index fcf024d..0000000 --- a/server/internal/v2/queue/state.go +++ /dev/null @@ -1,34 +0,0 @@ -package queue - -import ( - "sync/atomic" -) - -const ( - StatusNone = iota - 1 // 队列未运行 - StatusRunning // 队列运行中 - StatusClosing // 队列关闭中 - StatusClosed // 队列已关闭 -) - -type State[Id, Q comparable, M Message[Q]] struct { - queue *Queue[Id, Q, M] - id Id // 队列 ID - status int32 // 状态标志 - total int64 // 消息总计数 -} - -// IsClosed 判断队列是否已关闭 -func (q *State[Id, Q, M]) IsClosed() bool { - return atomic.LoadInt32(&q.status) == StatusClosed -} - -// IsClosing 判断队列是否正在关闭 -func (q *State[Id, Q, M]) IsClosing() bool { - return atomic.LoadInt32(&q.status) == StatusClosing -} - -// IsRunning 判断队列是否正在运行 -func (q *State[Id, Q, M]) IsRunning() bool { - return atomic.LoadInt32(&q.status) == StatusRunning -} diff --git a/server/internal/v2/queue/wrapper.go b/server/internal/v2/queue/wrapper.go deleted file mode 100644 index d81471d..0000000 --- a/server/internal/v2/queue/wrapper.go +++ /dev/null @@ -1,13 +0,0 @@ -package queue - -func newWrapper[Id, Q comparable, M Message[Q]](queue *Queue[Id, Q, M], message M) wrapper[Id, Q, M] { - return wrapper[Id, Q, M]{ - message: message, - controller: newController[Id, Q, M](queue, message), - } -} - -type wrapper[Id, Q comparable, M Message[Q]] struct { - message M - controller Controller -} diff --git a/server/internal/v2/reactor/handlers.go b/server/internal/v2/reactor/handlers.go deleted file mode 100644 index 755ef8a..0000000 --- a/server/internal/v2/reactor/handlers.go +++ /dev/null @@ -1,7 +0,0 @@ -package reactor - -import "github.com/kercylan98/minotaur/server/internal/v2/queue" - -type MessageHandler[Q comparable, M queue.Message[Q]] func(message M) - -type ErrorHandler[Q comparable, M queue.Message[Q]] func(message M, err error) diff --git a/server/internal/v2/reactor/reactor.go b/server/internal/v2/reactor/reactor.go deleted file mode 100644 index c537721..0000000 --- a/server/internal/v2/reactor/reactor.go +++ /dev/null @@ -1,209 +0,0 @@ -package reactor - -import ( - "fmt" - "github.com/kercylan98/minotaur/server/internal/v2/loadbalancer" - "github.com/kercylan98/minotaur/server/internal/v2/queue" - "github.com/kercylan98/minotaur/utils/log/v2" - "github.com/kercylan98/minotaur/utils/super" - "runtime" - "runtime/debug" - "sync" - "sync/atomic" -) - -const ( - statusNone = iota - 1 // 事件循环未运行 - statusRunning // 事件循环运行中 - statusClosing // 事件循环关闭中 - statusClosed // 事件循环已关闭 -) - -// NewReactor 创建一个新的 Reactor 实例,初始化系统级别的队列和多个 Socket 对应的队列 -func NewReactor[Queue comparable, M queue.Message[Queue]](queueSize, queueBufferSize int, handler MessageHandler[Queue, M], errorHandler ErrorHandler[Queue, M]) *Reactor[Queue, M] { - if handler == nil { - - } - r := &Reactor[Queue, M]{ - lb: loadbalancer.NewRoundRobin[int, *queue.Queue[int, Queue, M]](), - errorHandler: errorHandler, - queueSize: queueSize, - queueBufferSize: queueBufferSize, - state: statusNone, - handler: handler, - location: make(map[Queue]int), - } - r.logger.Store(log.GetLogger()) - - defaultNum := runtime.NumCPU() - r.queueRW.Lock() - for i := 0; i < defaultNum; i++ { - r.noneLockAddQueue() - } - r.queueRW.Unlock() - - return r -} - -// Reactor 是一个消息反应器,管理系统级别的队列和多个 Socket 对应的队列 -type Reactor[Queue comparable, M queue.Message[Queue]] struct { - logger atomic.Pointer[log.Logger] // 日志记录器 - state int32 // 状态 - queueSize int // 队列管道大小 - queueBufferSize int // 队列缓冲区大小 - queues []*queue.Queue[int, Queue, M] // 所有使用的队列 - queueRW sync.RWMutex // 队列读写锁 - location map[Queue]int // 所在队列 ID 映射 - locationRW sync.RWMutex // 所在队列 ID 映射锁 - lb *loadbalancer.RoundRobin[int, *queue.Queue[int, Queue, M]] // 负载均衡器 - wg sync.WaitGroup // 等待组 - cwg sync.WaitGroup // 关闭等待组 - handler MessageHandler[Queue, M] // 消息处理器 - errorHandler ErrorHandler[Queue, M] // 错误处理器 -} - -// SetLogger 设置日志记录器 -func (r *Reactor[Queue, M]) SetLogger(logger *log.Logger) { - r.logger.Store(logger) -} - -// GetLogger 获取日志记录器 -func (r *Reactor[Queue, M]) GetLogger() *log.Logger { - return r.logger.Load() -} - -// process 消息处理 -func (r *Reactor[Queue, M]) process(msg M) { - defer func(msg M) { - if err := super.RecoverTransform(recover()); err != nil { - if r.errorHandler != nil { - r.errorHandler(msg, err) - } else { - r.GetLogger().Error("Reactor", log.String("action", "process"), log.Any("queue", msg.GetQueue()), log.Err(err)) - debug.PrintStack() - } - } - }(msg) - - r.handler(msg) -} - -// Dispatch 将消息分发到 ident 使用的队列,当 ident 首次使用时,将会根据负载均衡策略选择一个队列 -// - 设置 count 会增加消息的外部计数,当 Reactor 关闭时会等待外部计数归零 -// - 当 ident 为空字符串时候,将发送到 -func (r *Reactor[Queue, M]) Dispatch(ident Queue, msg M) error { - r.queueRW.RLock() - if atomic.LoadInt32(&r.state) > statusClosing { - r.queueRW.RUnlock() - return fmt.Errorf("reactor closing or closed") - } - - var next *queue.Queue[int, Queue, M] - r.locationRW.RLock() - i, exist := r.location[ident] - r.locationRW.RUnlock() - if !exist { - r.locationRW.Lock() - if i, exist = r.location[ident]; !exist { - next = r.lb.Next() - r.location[ident] = next.Id() - r.logger.Load().Debug("Reactor", log.String("action", "bind"), log.Any("ident", ident), log.Any("queue", next.Id())) - } else { - next = r.queues[i] - } - r.locationRW.Unlock() - } else { - next = r.queues[i] - } - r.queueRW.RUnlock() - return next.Push(ident, msg) -} - -// Run 启动 Reactor,运行系统级别的队列和多个 Socket 对应的队列 -func (r *Reactor[Queue, M]) Run(callbacks ...func(queues []*queue.Queue[int, Queue, M])) { - if !atomic.CompareAndSwapInt32(&r.state, statusNone, statusRunning) { - return - } - r.queueRW.Lock() - queues := r.queues - for _, q := range queues { - r.runQueue(q) - } - r.queueRW.Unlock() - for _, callback := range callbacks { - callback(queues) - } - r.wg.Wait() -} - -func (r *Reactor[Queue, M]) noneLockAddQueue() { - q := queue.New[int, Queue, M](len(r.queues), r.queueSize, r.queueBufferSize) - r.lb.Add(q) // 运行前添加到负载均衡器,未运行时允许接收消息 - r.queues = append(r.queues, q) -} - -func (r *Reactor[Queue, M]) noneLockDelQueue(q *queue.Queue[int, Queue, M]) { - idx := q.Id() - if idx < 0 || idx >= len(r.queues) || r.queues[idx] != q { - return - } - r.queues = append(r.queues[:idx], r.queues[idx+1:]...) - for i := idx; i < len(r.queues); i++ { - r.queues[i].SetId(i) - } -} - -func (r *Reactor[Queue, M]) runQueue(q *queue.Queue[int, Queue, M]) { - r.wg.Add(1) - q.SetClosedHandler(func(q *queue.Queue[int, Queue, M]) { - // 关闭时正在等待关闭完成,外部已加锁,无需再次加锁 - r.noneLockDelQueue(q) - r.cwg.Done() - r.logger.Load().Debug("Reactor", log.String("action", "close"), log.Any("queue", q.Id())) - }) - go q.Run() - - go func(r *Reactor[Queue, M], q *queue.Queue[int, Queue, M]) { - defer r.wg.Done() - for m := range q.Read() { - m(r.process, r.processFinish) - } - }(r, q) -} - -func (r *Reactor[Queue, M]) Close() { - if !atomic.CompareAndSwapInt32(&r.state, statusRunning, statusClosing) { - return - } - r.queueRW.Lock() - r.cwg.Add(len(r.queues) + 1) - for _, q := range r.queues { - q.Close() - } - r.cwg.Wait() - atomic.StoreInt32(&r.state, statusClosed) - r.queueRW.Unlock() -} - -func (r *Reactor[Queue, M]) processFinish(m M, last bool) { - if !last { - return - } - queueName := m.GetQueue() - - r.locationRW.RLock() - mq, exist := r.location[queueName] - r.locationRW.RUnlock() - if exist { - r.locationRW.Lock() - defer r.locationRW.Unlock() - mq, exist = r.location[queueName] - if exist { - delete(r.location, queueName) - r.queueRW.RLock() - mq := r.queues[mq] - r.queueRW.RUnlock() - r.logger.Load().Debug("Reactor", log.String("action", "unbind"), log.Any("queueName", queueName), log.Any("queue", mq.Id())) - } - } -} diff --git a/server/internal/v2/reactor/reactor_test.go b/server/internal/v2/reactor/reactor_test.go deleted file mode 100644 index e557d9f..0000000 --- a/server/internal/v2/reactor/reactor_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package reactor_test - -import ( - "github.com/kercylan98/minotaur/server/internal/v2/queue" - "github.com/kercylan98/minotaur/server/internal/v2/reactor" - "github.com/kercylan98/minotaur/utils/log/v2" - "github.com/kercylan98/minotaur/utils/random" - "github.com/kercylan98/minotaur/utils/times" - "os" - "testing" - "time" -) - -func BenchmarkReactor_Dispatch(b *testing.B) { - var r = reactor.NewReactor(1024*16, 1024, 1024, 1024, func(message queue.MessageWrapper[int, string, func()]) { - message.Message() - }, func(message queue.MessageWrapper[int, string, func()], err error) { - - }) - - r.SetLogger(log.NewLogger(log.NewHandler(os.Stdout, log.DefaultOptions().WithLevel(log.LevelInfo)))) - - go r.Run() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - if err := r.IdentDispatch(random.HostName(), func() { - }); err != nil { - return - } - } - }) -} - -func TestReactor_Dispatch(t *testing.T) { - var r = reactor.NewReactor(1024*16, 1024, 1024, 1024, func(message queue.MessageWrapper[int, string, func()]) { - message.Message() - }, func(message queue.MessageWrapper[int, string, func()], err error) { - - }) - - go r.Run() - - for i := 0; i < 10000; i++ { - go func() { - id := random.HostName() - for { - time.Sleep(time.Millisecond * 20) - if err := r.IdentDispatch(id, func() { - - }); err != nil { - return - } - } - }() - } - - time.Sleep(times.Second) - r.Close() -} diff --git a/server/internal/v2/server.go b/server/internal/v2/server.go index 771a3a9..39ccf4b 100644 --- a/server/internal/v2/server.go +++ b/server/internal/v2/server.go @@ -3,9 +3,10 @@ package server import ( "context" "fmt" - "github.com/kercylan98/minotaur/server/internal/v2/message" - "github.com/kercylan98/minotaur/server/internal/v2/message/messages" - "github.com/kercylan98/minotaur/server/internal/v2/queue" + "github.com/kercylan98/minotaur/toolkit/message" + "github.com/kercylan98/minotaur/toolkit/message/brokers" + messageEvents "github.com/kercylan98/minotaur/toolkit/message/events" + "github.com/kercylan98/minotaur/toolkit/message/queues" "github.com/kercylan98/minotaur/utils/collection" "github.com/kercylan98/minotaur/utils/log/v2" "github.com/kercylan98/minotaur/utils/random" @@ -13,13 +14,11 @@ import ( "reflect" "time" - "github.com/kercylan98/minotaur/server/internal/v2/reactor" "github.com/kercylan98/minotaur/utils/network" ) type Server interface { Events - message.Broker[Producer, string] // Run 运行服务器 Run() error @@ -31,10 +30,10 @@ type Server interface { GetStatus() *State // PublishSyncMessage 发布同步消息 - PublishSyncMessage(queue string, handler messages.SynchronousHandler[Producer, string, Server]) + PublishSyncMessage(topic string, handler messageEvents.SynchronousHandler) // PublishAsyncMessage 发布异步消息,当包含多个 callback 时,仅首个生效 - PublishAsyncMessage(queue string, handler messages.AsynchronousHandler[Producer, string, Server], callback ...messages.AsynchronousCallbackHandler[Producer, string, Server]) + PublishAsyncMessage(topic string, handler messageEvents.AsynchronousHandler, callback ...messageEvents.AsynchronousCallbackHandler) } type server struct { @@ -48,7 +47,7 @@ type server struct { ctx context.Context cancel context.CancelFunc network Network - reactor *reactor.Reactor[string, message.Message[Producer, string]] + broker message.Broker[int, string] } func NewServer(network Network, options ...*Options) Server { @@ -62,17 +61,12 @@ func NewServer(network Network, options ...*Options) Server { srv.controller = new(controller).init(srv) srv.events = new(events).init(srv) srv.state = new(State).init(srv) - srv.reactor = reactor.NewReactor[string, message.Message[Producer, string]]( - srv.GetActorMessageChannelSize(), - srv.GetActorMessageBufferInitialSize(), - srv.onProcessMessage, - func(message message.Message[Producer, string], err error) { - if handler := srv.GetMessageErrorHandler(); handler != nil { - handler(srv, message, err) - } - }) + srv.broker = brokers.NewSparseGoroutine(func(index int) message.Queue[int, string] { + return queues.NewNonBlockingRW[int, string](index, 1024*8, 1024) + }, func(handler message.EventExecutor) { + handler() + }) srv.Options.init(srv).Apply(options...) - srv.reactor.SetLogger(srv.Options.GetLogger()) antsPool, err := ants.NewPool(ants.DefaultAntsPoolSize, ants.WithOptions(ants.Options{ ExpiryDuration: 10 * time.Second, Nonblocking: true, @@ -89,15 +83,7 @@ func NewServer(network Network, options ...*Options) Server { } func (s *server) Run() (err error) { - var queueWait = make(chan struct{}) - go s.reactor.Run(func(queues []*queue.Queue[int, string, message.Message[Producer, string]]) { - for _, q := range queues { - s.GetLogger().Debug("Reactor", log.String("action", "run"), log.Any("queue", q.Id())) - } - close(queueWait) - }) - <-queueWait - + go s.broker.Run() if err = s.network.OnSetup(s.ctx, s); err != nil { return } @@ -125,7 +111,7 @@ func (s *server) Shutdown() (err error) { DisableHttpPProf() s.events.onShutdown() err = s.network.OnShutdown() - s.reactor.Close() + s.broker.Close() return } @@ -133,38 +119,22 @@ func (s *server) getSysQueue() string { return s.queue } -func (s *server) PublishMessage(msg message.Message[Producer, string]) { - s.reactor.Dispatch(msg.GetQueue(), msg) +func (s *server) PublishMessage(topic string, event message.Event[int, string]) { + s.broker.Publish(topic, event) } -func (s *server) PublishSyncMessage(queue string, handler messages.SynchronousHandler[Producer, string, Server]) { - s.PublishMessage(messages.Synchronous[Producer, string, Server]( - s, newProducer(s, nil), queue, - handler, - )) +func (s *server) PublishSyncMessage(topic string, handler messageEvents.SynchronousHandler) { + s.PublishMessage(topic, messageEvents.Synchronous[int, string](handler)) } -func (s *server) PublishAsyncMessage(queue string, handler messages.AsynchronousHandler[Producer, string, Server], callback ...messages.AsynchronousCallbackHandler[Producer, string, Server]) { - s.PublishMessage(messages.Asynchronous[Producer, string, Server]( - s, newProducer(s, nil), queue, - func(ctx context.Context, srv Server, f func(context.Context, Server)) { - s.ants.Submit(func() { - f(ctx, s) - }) - }, - handler, - collection.FindFirstOrDefaultInSlice(callback, nil), - )) +func (s *server) PublishAsyncMessage(topic string, handler messageEvents.AsynchronousHandler, callback ...messageEvents.AsynchronousCallbackHandler) { + s.PublishMessage(topic, messageEvents.Asynchronous[int, string](func(ctx context.Context, f func(context.Context)) { + s.ants.Submit(func() { + f(ctx) + }) + }, handler, collection.FindFirstOrDefaultInSlice(callback, nil))) } func (s *server) GetStatus() *State { return s.state.Status() } - -func (s *server) onProcessMessage(m message.Message[Producer, string]) { - s.getManyOptions(func(opt *Options) { - ctx := context.Background() - m.OnInitialize(ctx) - m.OnProcess() - }) -} diff --git a/server/internal/v2/server_test.go b/server/internal/v2/server_test.go index 9ff3ccc..947d97d 100644 --- a/server/internal/v2/server_test.go +++ b/server/internal/v2/server_test.go @@ -35,9 +35,9 @@ func TestNewServer(t *testing.T) { t.Error(err) } - srv.PublishAsyncMessage("123", func(ctx context.Context, s server.Server) error { + srv.PublishAsyncMessage("123", func(ctx context.Context) error { return nil - }, func(ctx context.Context, s server.Server, err error) { + }, func(ctx context.Context, err error) { for i := 0; i < 10000000; i++ { _ = tm["1"] tm["1"] = random.Bool() diff --git a/toolkit/chrono/adjuster.go b/toolkit/chrono/adjuster.go new file mode 100644 index 0000000..8f26ed6 --- /dev/null +++ b/toolkit/chrono/adjuster.go @@ -0,0 +1,38 @@ +package chrono + +import "time" + +// NewAdjuster 创建一个时间调节器 +func NewAdjuster(adjust time.Duration) *Adjuster { + return &Adjuster{adjust: adjust} +} + +// Adjuster 时间调节器是一个用于获取偏移时间的工具 +type Adjuster struct { + adjust time.Duration +} + +// Adjust 获取偏移调整的时间量 +func (a *Adjuster) Adjust() time.Duration { + return a.adjust +} + +// SetAdjust 设置偏移调整的时间量 +func (a *Adjuster) SetAdjust(adjust time.Duration) { + a.adjust = adjust +} + +// AddAdjust 增加偏移调整的时间量 +func (a *Adjuster) AddAdjust(adjust time.Duration) { + a.adjust += adjust +} + +// Now 获取经过偏移调整的当前时间 +func (a *Adjuster) Now() time.Time { + return time.Now().Add(a.adjust) +} + +// Since 获取经过偏移调整的时间间隔 +func (a *Adjuster) Since(t time.Time) time.Duration { + return a.Now().Sub(t) +} diff --git a/toolkit/chrono/adjuster_built_in.go b/toolkit/chrono/adjuster_built_in.go new file mode 100644 index 0000000..30b0eb9 --- /dev/null +++ b/toolkit/chrono/adjuster_built_in.go @@ -0,0 +1,26 @@ +package chrono + +import "time" + +var ( + builtInAdjuster *Adjuster +) + +func init() { + builtInAdjuster = NewAdjuster(0) +} + +// BuiltInAdjuster 获取内置的由 NewAdjuster(0) 函数创建的时间调节器 +func BuiltInAdjuster() *Adjuster { + return builtInAdjuster +} + +// Now 调用内置时间调节器 BuiltInAdjuster 的 Adjuster.Now 函数 +func Now() time.Time { + return BuiltInAdjuster().Now() +} + +// Since 调用内置时间调节器 BuiltInAdjuster 的 Adjuster.Since 函数 +func Since(t time.Time) time.Duration { + return BuiltInAdjuster().Since(t) +} diff --git a/toolkit/chrono/constants.go b/toolkit/chrono/constants.go new file mode 100644 index 0000000..12df9ec --- /dev/null +++ b/toolkit/chrono/constants.go @@ -0,0 +1,14 @@ +package chrono + +import "time" + +const ( + Nanosecond = time.Nanosecond + Microsecond = time.Microsecond + Millisecond = time.Millisecond + Second = time.Second + Minute = time.Minute + Hour = time.Hour + Day = Hour * 24 + Week = Day * 7 +) diff --git a/toolkit/chrono/moment.go b/toolkit/chrono/moment.go new file mode 100644 index 0000000..c7ddff1 --- /dev/null +++ b/toolkit/chrono/moment.go @@ -0,0 +1,350 @@ +package chrono + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "math" + "time" +) + +var zero = time.Time{} + +// IsMomentReached 检查指定时刻是否已到达且未发生过 +// - now: 当前时间 +// - last: 上次发生的时间 +// - hour: 要检查的时刻的小时数 +// - min: 要检查的时刻的分钟数 +// - sec: 要检查的时刻的秒数 +func IsMomentReached(now time.Time, last time.Time, hour, min, sec int) bool { + moment := time.Date(last.Year(), last.Month(), last.Day(), hour, min, sec, 0, time.Local) + if !moment.Before(now) { + return false + } else if moment.After(last) { + return true + } + + // 如果要检查的时刻在上次发生的时间和当前时间之间,并且已经过了一天,说明已经发生 + nextDayMoment := moment.AddDate(0, 0, 1) + return !nextDayMoment.After(now) +} + +// GetNextMoment 获取下一个指定时刻发生的时间。 +func GetNextMoment(now time.Time, hour, min, sec int) time.Time { + moment := time.Date(now.Year(), now.Month(), now.Day(), hour, min, sec, 0, time.Local) + // 如果要检查的时刻已经过了,则返回明天的这个时刻 + if moment.Before(now) { + moment = moment.AddDate(0, 0, 1) + } + return moment +} + +// IsMomentInDays 检查指定时刻是否在给定的天数内发生。 +// - now: 当前时间 +// - hour: 要检查的时刻的小时数 +// - min: 要检查的时刻的分钟数 +// - sec: 要检查的时刻的秒数 +// - days: 表示要偏移的天数。正数表示未来,负数表示过去,0 即今天 +func IsMomentInDays(now time.Time, hour, min, sec, days int) bool { + offsetTime := now.AddDate(0, 0, days) + moment := time.Date(offsetTime.Year(), offsetTime.Month(), offsetTime.Day(), hour, min, sec, 0, time.Local) + return now.Before(moment.AddDate(0, 0, 1)) && now.After(moment) +} + +// IsMomentYesterday 检查指定时刻是否在昨天发生 +func IsMomentYesterday(now time.Time, hour, min, sec int) bool { + return IsMomentInDays(now, hour, min, sec, -1) +} + +// IsMomentToday 检查指定时刻是否在今天发生 +func IsMomentToday(now time.Time, hour, min, sec int) bool { + return IsMomentInDays(now, hour, min, sec, 0) +} + +// IsMomentTomorrow 检查指定时刻是否在明天发生 +func IsMomentTomorrow(now time.Time, hour, min, sec int) bool { + return IsMomentInDays(now, hour, min, sec, 1) +} + +// IsMomentPassed 检查指定时刻是否已经过去 +func IsMomentPassed(now time.Time, hour, min, sec int) bool { + // 构建要检查的时刻 + moment := time.Date(now.Year(), now.Month(), now.Day(), hour, min, sec, 0, time.Local) + return now.After(moment) +} + +// IsMomentFuture 检查指定时刻是否在未来 +func IsMomentFuture(now time.Time, hour, min, sec int) bool { + // 构建要检查的时刻 + moment := time.Date(now.Year(), now.Month(), now.Day(), hour, min, sec, 0, time.Local) + return now.Before(moment) +} + +// GetStartOfDay 获取指定时间的当天第一刻,即 00:00:00 +func GetStartOfDay(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) +} + +// GetEndOfDay 获取指定时间的当天最后一刻,即 23:59:59 +func GetEndOfDay(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location()) +} + +// GetRelativeStartOfDay 获取相对于指定时间减去或加上指定天数后的当天开始时间 +// - offsetDays: 要偏移的天数,负数表示过去的某一天,正数表示未来的某一天 +func GetRelativeStartOfDay(t time.Time, offsetDays int) time.Time { + return GetStartOfDay(GetStartOfDay(t.AddDate(0, 0, offsetDays))) +} + +// GetRelativeEndOfDay 获取相对于指定时间减去或加上指定天数后的当天结束时间 +// - offsetDays: 要偏移的天数,负数表示过去的某一天,正数表示未来的某一天 +func GetRelativeEndOfDay(t time.Time, offsetDays int) time.Time { + return GetEndOfDay(GetEndOfDay(t.AddDate(0, 0, offsetDays))) +} + +// GetStartOfWeek 获取指定时间所在周的特定周的开始时刻,即 00:00:00 +func GetStartOfWeek(t time.Time, weekday time.Weekday) time.Time { + t = GetStartOfDay(t) + tw := t.Weekday() + if tw == 0 { + tw = 7 + } + d := 1 - int(tw) + switch weekday { + case time.Sunday: + d += 6 + default: + d += int(weekday) - 1 + } + return t.AddDate(0, 0, d) +} + +// GetEndOfWeek 获取指定时间所在周的特定周的最后时刻,即 23:59:59 +func GetEndOfWeek(t time.Time, weekday time.Weekday) time.Time { + return GetEndOfDay(GetStartOfWeek(t, weekday)) +} + +// GetRelativeStartOfWeek 获取相对于当前时间的本周开始时间,以指定的星期作为一周的开始,并根据需要进行周数的偏移 +// - now:当前时间 +// - week:以哪一天作为一周的开始 +// - offsetWeeks:要偏移的周数,正数表示向未来偏移,负数表示向过去偏移 +// +// 该函数返回以指定星期作为一周的开始时间,然后根据偏移量进行周数偏移,得到相对于当前时间的周的开始时间 +// +// 假设 week 为 time.Saturday 且 offsetWeeks 为 -1,则表示获取上周六的开始时间,一下情况中第一个时间为 now,第二个时间为函数返回值 +// - 2024-03-01 00:00:00 --相对时间-> 2024-02-24 00:00:00 --偏移时间--> 2024-02-17 00:00:00 +// - 2024-03-02 00:00:00 --相对时间-> 2024-02-24 00:00:00 --偏移时间--> 2024-02-17 00:00:00 +// - 2024-03-03 00:00:00 --相对时间-> 2024-03-02 00:00:00 --偏移时间--> 2024-02-24 00:00:00 +func GetRelativeStartOfWeek(now time.Time, week time.Weekday, offsetWeeks int) time.Time { + nowWeekday, weekday := int(now.Weekday()), int(week) + if nowWeekday == 0 { + nowWeekday = 7 + } + if weekday == 0 { + weekday = 7 + } + if nowWeekday < weekday { + now = now.Add(-Week) + } + moment := GetStartOfWeek(now, week) + return moment.Add(Week * time.Duration(offsetWeeks)) +} + +// GetRelativeEndOfWeek 获取相对于当前时间的本周结束时间,以指定的星期作为一周的开始,并根据需要进行周数的偏移 +// - 该函数详细解释参考 GetRelativeEndOfWeek 函数,其中不同的是,该函数返回的是这一天最后一刻的时间,即 23:59:59 +func GetRelativeEndOfWeek(now time.Time, week time.Weekday, offsetWeeks int) time.Time { + return GetEndOfDay(GetRelativeStartOfWeek(now, week, offsetWeeks)) +} + +// GetRelativeTimeOfWeek 获取相对于当前时间的本周指定星期的指定时刻,以指定的星期作为一周的开始,并根据需要进行周数的偏移 +// - 该函数详细解释参考 GetRelativeStartOfWeek 函数,其中不同的是,该函数返回的是这一天对应 now 的时间 +func GetRelativeTimeOfWeek(now time.Time, week time.Weekday, offsetWeeks int) time.Time { + moment := GetRelativeStartOfWeek(now, week, offsetWeeks) + return time.Date(moment.Year(), moment.Month(), moment.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), now.Location()) +} + +// Zero 获取一个零值的时间 +func Zero() time.Time { + return zero +} + +// IsZero 检查一个时间是否为零值 +func IsZero(t time.Time) bool { + return t.IsZero() +} + +// Max 获取两个时间中的最大值 +func Max(t1, t2 time.Time) time.Time { + if t1.After(t2) { + return t1 + } + return t2 +} + +// Min 获取两个时间中的最小值 +func Min(t1, t2 time.Time) time.Time { + if t1.Before(t2) { + return t1 + } + return t2 +} + +// SmallerFirst 将两个时间按照从小到大的顺序排列 +func SmallerFirst(t1, t2 time.Time) (time.Time, time.Time) { + if t1.Before(t2) { + return t1, t2 + } + return t2, t1 +} + +// SmallerLast 将两个时间按照从大到小的顺序排列 +func SmallerLast(t1, t2 time.Time) (time.Time, time.Time) { + if t1.Before(t2) { + return t2, t1 + } + return t1, t2 +} + +// Delta 获取两个时间之间的时间差 +func Delta(t1, t2 time.Time) time.Duration { + if t1.Before(t2) { + return t2.Sub(t1) + } + return t1.Sub(t2) +} + +// FloorDeltaDays 计算两个时间之间的天数差异,并向下取整 +func FloorDeltaDays(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Day) +} + +// CeilDeltaDays 计算两个时间之间的天数差异,并向上取整 +func CeilDeltaDays(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Ceil(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Day))) +} + +// RoundDeltaDays 计算两个时间之间的天数差异,并四舍五入 +func RoundDeltaDays(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Round(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Day))) +} + +// FloorDeltaHours 计算两个时间之间的小时数差异,并向下取整 +func FloorDeltaHours(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Hour) +} + +// CeilDeltaHours 计算两个时间之间的小时数差异,并向上取整 +func CeilDeltaHours(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Ceil(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Hour))) +} + +// RoundDeltaHours 计算两个时间之间的小时数差异,并四舍五入 +func RoundDeltaHours(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Round(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Hour))) +} + +// FloorDeltaMinutes 计算两个时间之间的分钟数差异,并向下取整 +func FloorDeltaMinutes(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Minute) +} + +// CeilDeltaMinutes 计算两个时间之间的分钟数差异,并向上取整 +func CeilDeltaMinutes(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Ceil(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Minute))) +} + +// RoundDeltaMinutes 计算两个时间之间的分钟数差异,并四舍五入 +func RoundDeltaMinutes(t1, t2 time.Time) int { + t1, t2 = SmallerFirst(t1, t2) + return int(math.Round(float64(GetStartOfDay(t2).Sub(GetStartOfDay(t1)) / Minute))) +} + +// IsSameSecond 检查两个时间是否在同一秒 +func IsSameSecond(t1, t2 time.Time) bool { + return t1.Unix() == t2.Unix() +} + +// IsSameMinute 检查两个时间是否在同一分钟 +func IsSameMinute(t1, t2 time.Time) bool { + return t1.Minute() == t2.Minute() && IsSameHour(t1, t2) +} + +// IsSameHour 检查两个时间是否在同一小时 +func IsSameHour(t1, t2 time.Time) bool { + return t1.Hour() == t2.Hour() && IsSameDay(t1, t2) +} + +// IsSameDay 检查两个时间是否在同一天 +func IsSameDay(t1, t2 time.Time) bool { + return GetStartOfDay(t1).Equal(GetStartOfDay(t2)) +} + +// IsSameWeek 检查两个时间是否在同一周 +func IsSameWeek(t1, t2 time.Time) bool { + return GetStartOfWeek(t1, time.Monday).Equal(GetStartOfWeek(t2, time.Monday)) +} + +// IsSameMonth 检查两个时间是否在同一月 +func IsSameMonth(t1, t2 time.Time) bool { + return t1.Month() == t2.Month() && t1.Year() == t2.Year() +} + +// IsSameYear 检查两个时间是否在同一年 +func IsSameYear(t1, t2 time.Time) bool { + return t1.Year() == t2.Year() +} + +// GetMonthDays 获取指定时间所在月的天数 +func GetMonthDays(t time.Time) int { + year, month, _ := t.Date() + if month != 2 { + if month == 4 || month == 6 || month == 9 || month == 11 { + return 30 + } + return 31 + } + if ((year%4 == 0) && (year%100 != 0)) || year%400 == 0 { + return 29 + } + return 28 +} + +// ToDuration 将一个数值转换为 time.Duration 类型,当 unit 为空时,默认为纳秒单位 +func ToDuration[V generic.Number](v V, unit ...time.Duration) time.Duration { + var u = Nanosecond + if len(unit) > 0 { + u = unit[0] + } + return time.Duration(v) * u +} + +// ToDurationSecond 将一个数值转换为秒的 time.Duration 类型 +func ToDurationSecond[V generic.Number](v V) time.Duration { + return ToDuration(v, Second) +} + +// ToDurationMinute 将一个数值转换为分钟的 time.Duration 类型 +func ToDurationMinute[V generic.Number](v V) time.Duration { + return ToDuration(v, Minute) +} + +// ToDurationHour 将一个数值转换为小时的 time.Duration 类型 +func ToDurationHour[V generic.Number](v V) time.Duration { + return ToDuration(v, Hour) +} + +// ToDurationDay 将一个数值转换为天的 time.Duration 类型 +func ToDurationDay[V generic.Number](v V) time.Duration { + return ToDuration(v, Day) +} + +// ToDurationWeek 将一个数值转换为周的 time.Duration 类型 +func ToDurationWeek[V generic.Number](v V) time.Duration { + return ToDuration(v, Week) +} diff --git a/toolkit/chrono/period.go b/toolkit/chrono/period.go new file mode 100644 index 0000000..288fe67 --- /dev/null +++ b/toolkit/chrono/period.go @@ -0,0 +1,172 @@ +package chrono + +import ( + "time" +) + +// NewPeriod 创建一个时间段 +// - 如果 start 比 end 晚,则会自动交换两个时间 +func NewPeriod(start, end time.Time) Period { + if start.After(end) { + start, end = end, start + } + return Period{start, end} +} + +// NewPeriodWindow 创建一个特定长度的时间窗口 +func NewPeriodWindow(t time.Time, size time.Duration) Period { + start := t.Truncate(size) + end := start.Add(size) + return Period{start, end} +} + +// NewPeriodWindowWeek 创建一周长度的时间窗口,从周一零点开始至周日 23:59:59 结束 +func NewPeriodWindowWeek(t time.Time) Period { + var start = GetStartOfWeek(t, time.Monday) + end := start.Add(Week) + return Period{start, end} +} + +// NewPeriodWithTimeArray 创建一个时间段 +func NewPeriodWithTimeArray(times [2]time.Time) Period { + return NewPeriod(times[0], times[1]) +} + +// NewPeriodWithDayZero 创建一个时间段,从 t 开始,持续到 day 天后的 0 点 +func NewPeriodWithDayZero(t time.Time, day int) Period { + return NewPeriod(t, GetStartOfDay(t.AddDate(0, 0, day))) +} + +// NewPeriodWithDay 创建一个时间段,从 t 开始,持续 day 天 +func NewPeriodWithDay(t time.Time, day int) Period { + return NewPeriod(t, t.AddDate(0, 0, day)) +} + +// NewPeriodWithHour 创建一个时间段,从 t 开始,持续 hour 小时 +func NewPeriodWithHour(t time.Time, hour int) Period { + return NewPeriod(t, t.Add(time.Duration(hour)*time.Hour)) +} + +// NewPeriodWithMinute 创建一个时间段,从 t 开始,持续 minute 分钟 +func NewPeriodWithMinute(t time.Time, minute int) Period { + return NewPeriod(t, t.Add(time.Duration(minute)*time.Minute)) +} + +// NewPeriodWithSecond 创建一个时间段,从 t 开始,持续 second 秒 +func NewPeriodWithSecond(t time.Time, second int) Period { + return NewPeriod(t, t.Add(time.Duration(second)*time.Second)) +} + +// NewPeriodWithMillisecond 创建一个时间段,从 t 开始,持续 millisecond 毫秒 +func NewPeriodWithMillisecond(t time.Time, millisecond int) Period { + return NewPeriod(t, t.Add(time.Duration(millisecond)*time.Millisecond)) +} + +// NewPeriodWithMicrosecond 创建一个时间段,从 t 开始,持续 microsecond 微秒 +func NewPeriodWithMicrosecond(t time.Time, microsecond int) Period { + return NewPeriod(t, t.Add(time.Duration(microsecond)*time.Microsecond)) +} + +// NewPeriodWithNanosecond 创建一个时间段,从 t 开始,持续 nanosecond 纳秒 +func NewPeriodWithNanosecond(t time.Time, nanosecond int) Period { + return NewPeriod(t, t.Add(time.Duration(nanosecond)*time.Nanosecond)) +} + +// Period 表示一个时间段 +type Period [2]time.Time + +// Start 返回时间段的开始时间 +func (p Period) Start() time.Time { + return p[0] +} + +// End 返回时间段的结束时间 +func (p Period) End() time.Time { + return p[1] +} + +// Duration 返回时间段的持续时间 +func (p Period) Duration() time.Duration { + return p[1].Sub(p[0]) +} + +// Days 返回时间段的持续天数 +func (p Period) Days() int { + return int(p.Duration().Hours() / 24) +} + +// Hours 返回时间段的持续小时数 +func (p Period) Hours() int { + return int(p.Duration().Hours()) +} + +// Minutes 返回时间段的持续分钟数 +func (p Period) Minutes() int { + return int(p.Duration().Minutes()) +} + +// Seconds 返回时间段的持续秒数 +func (p Period) Seconds() int { + return int(p.Duration().Seconds()) +} + +// Milliseconds 返回时间段的持续毫秒数 +func (p Period) Milliseconds() int { + return int(p.Duration().Milliseconds()) +} + +// Microseconds 返回时间段的持续微秒数 +func (p Period) Microseconds() int { + return int(p.Duration().Microseconds()) +} + +// Nanoseconds 返回时间段的持续纳秒数 +func (p Period) Nanoseconds() int { + return int(p.Duration().Nanoseconds()) +} + +// IsZero 判断时间段是否为零值 +func (p Period) IsZero() bool { + return p[0].IsZero() && p[1].IsZero() +} + +// IsInvalid 判断时间段是否无效 +func (p Period) IsInvalid() bool { + return p[0].IsZero() || p[1].IsZero() +} + +// IsBefore 判断时间段是否在指定时间之前 +func (p Period) IsBefore(t time.Time) bool { + return p[1].Before(t) +} + +// IsAfter 判断时间段是否在指定时间之后 +func (p Period) IsAfter(t time.Time) bool { + return p[0].After(t) +} + +// IsBetween 判断指定时间是否在时间段之间 +func (p Period) IsBetween(t time.Time) bool { + return p[0].Before(t) && p[1].After(t) +} + +// IsOngoing 判断指定时间是否正在进行时 +// - 如果时间段的开始时间在指定时间之前或者等于指定时间,且时间段的结束时间在指定时间之后,则返回 true +func (p Period) IsOngoing(t time.Time) bool { + return (p[0].Before(t) || p[0].Equal(t)) && p[1].After(t) +} + +// IsBetweenOrEqual 判断指定时间是否在时间段之间或者等于时间段的开始或结束时间 +func (p Period) IsBetweenOrEqual(t time.Time) bool { + return p.IsBetween(t) || p[0].Equal(t) || p[1].Equal(t) +} + +// IsBetweenOrEqualPeriod 判断指定时间是否在时间段之间或者等于时间段的开始或结束时间 +func (p Period) IsBetweenOrEqualPeriod(t Period) bool { + return p.IsBetween(t[0]) || p.IsBetween(t[1]) || p[0].Equal(t[0]) || p[1].Equal(t[1]) +} + +// IsOverlap 判断时间段是否与指定时间段重叠 +func (p Period) IsOverlap(t Period) bool { + return p.IsBetweenOrEqualPeriod(t) || t.IsBetweenOrEqualPeriod(p) +} diff --git a/toolkit/chrono/period_test.go b/toolkit/chrono/period_test.go new file mode 100644 index 0000000..8e8aaaa --- /dev/null +++ b/toolkit/chrono/period_test.go @@ -0,0 +1,15 @@ +package chrono_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/chrono" + "testing" + "time" +) + +func TestNewPeriodWindow(t *testing.T) { + cur := time.Now() + fmt.Println(cur) + window := chrono.NewPeriodWindow(cur, chrono.Day) + fmt.Println(window) +} diff --git a/toolkit/chrono/scheduler.go b/toolkit/chrono/scheduler.go new file mode 100644 index 0000000..d7ed3a1 --- /dev/null +++ b/toolkit/chrono/scheduler.go @@ -0,0 +1,218 @@ +package chrono + +import ( + "github.com/RussellLuo/timingwheel" + "github.com/gorhill/cronexpr" + "github.com/kercylan98/minotaur/utils/collection" + "reflect" + "sync" + "time" +) + +const ( + DefaultSchedulerTick = SchedulerPoolDefaultTick + DefaultSchedulerWheelSize = SchedulerPoolDefaultWheelSize +) + +const ( + SchedulerForever = -1 // 无限循环 + SchedulerOnce = 1 // 一次 + SchedulerInstantly = 0 // 立刻 +) + +// NewDefaultScheduler 创建一个默认的时间调度器 +// - tick: DefaultSchedulerTick +// - wheelSize: DefaultSchedulerWheelSize +func NewDefaultScheduler() *Scheduler { + return NewScheduler(DefaultSchedulerTick, DefaultSchedulerWheelSize) +} + +// NewScheduler 创建一个并发安全的时间调度器 +// - tick: 时间轮的刻度间隔。 +// - wheelSize: 时间轮的大小。 +func NewScheduler(tick time.Duration, wheelSize int64) *Scheduler { + return newScheduler(nil, tick, wheelSize) +} + +func newScheduler(pool *SchedulerPool, tick time.Duration, wheelSize int64) *Scheduler { + scheduler := &Scheduler{ + pool: pool, + wheel: timingwheel.NewTimingWheel(tick, wheelSize), + tasks: make(map[string]*schedulerTask), + } + if pool != nil { + scheduler.generation = pool.getGeneration() + } + scheduler.wheel.Start() + return scheduler +} + +// Scheduler 并发安全的时间调度器 +type Scheduler struct { + pool *SchedulerPool // 时间调度器所属的池,当该值为 nil 时,该时间调度器不属于任何池 + wheel *timingwheel.TimingWheel // 时间轮 + tasks map[string]*schedulerTask // 所有任务 + lock sync.RWMutex // 用于确保并发安全的锁 + generation int64 // 时间调度器的代数 + tick time.Duration // 时间周期 + + executor func(name string, caller func()) // 任务执行器 +} + +// SetExecutor 设置任务执行器 +// - 如果该任务执行器来自于时间调度器对象池,那么默认情况下将会使用时间调度器对象池的任务执行器,主动设置将会覆盖默认的任务执行器 +func (s *Scheduler) SetExecutor(executor func(name string, caller func())) { + s.lock.Lock() + defer s.lock.Unlock() + s.executor = executor +} + +// Release 释放时间调度器,时间调度器被释放后将不再可用,如果时间调度器属于某个池且池未满,则会重新加入到池中 +// - 释放后所有已注册的任务将会被取消 +func (s *Scheduler) Release() { + s.lock.Lock() + defer s.lock.Unlock() + for name, task := range s.tasks { + task.close() + delete(s.tasks, name) + } + + if s.pool == nil || s.pool.getGeneration() != s.generation { + s.wheel.Stop() + return + } + + s.pool.lock.Lock() + if len(s.pool.schedulers) < s.pool.size { + s.pool.schedulers = append(s.pool.schedulers, s) + s.pool.lock.Unlock() + return + } + s.pool.lock.Unlock() + s.wheel.Stop() +} + +// UnregisterTask 取消特定任务的执行计划的注册 +// - 如果任务不存在,则不执行任何操作 +func (s *Scheduler) UnregisterTask(name string) { + s.lock.Lock() + defer s.lock.Unlock() + if task, exist := s.tasks[name]; exist { + task.close() + delete(s.tasks, name) + } +} + +// GetRegisteredTasks 获取所有未执行完成的任务名称 +func (s *Scheduler) GetRegisteredTasks() []string { + s.lock.RLock() + defer s.lock.RUnlock() + return collection.ConvertMapKeysToSlice(s.tasks) +} + +// RegisterCronTask 通过 cron 表达式注册一个任务。 +// - 当 cron 表达式错误时,将会返回错误信息 +func (s *Scheduler) RegisterCronTask(name, expression string, function interface{}, args ...interface{}) error { + expr, err := cronexpr.Parse(expression) + if err != nil { + return err + } + s.task(name, 0, 0, expr, 0, function, args...) + return nil +} + +// RegisterImmediateCronTask 与 RegisterCronE 相同,但是会立即执行一次 +func (s *Scheduler) RegisterImmediateCronTask(name, expression string, function interface{}, args ...interface{}) error { + if err := s.RegisterCronTask(name, expression, function, args...); err != nil { + return err + } + s.call(name, function, args...) + return nil +} + +// RegisterAfterTask 注册一个在特定时间后执行一次的任务 +func (s *Scheduler) RegisterAfterTask(name string, after time.Duration, function interface{}, args ...interface{}) { + s.task(name, after, s.pool.tick, nil, 1, function, args...) +} + +// RegisterRepeatedTask 注册一个在特定时间后反复执行的任务 +func (s *Scheduler) RegisterRepeatedTask(name string, after, interval time.Duration, times int, function interface{}, args ...interface{}) { + s.task(name, after, interval, nil, times, function, args...) +} + +// RegisterDayMomentTask 注册一个在每天特定时刻执行的任务 +// - 其中 lastExecuted 为上次执行时间,adjust 为时间偏移量,hour、min、sec 为时、分、秒 +// - 当上次执行时间被错过时,将会立即执行一次 +func (s *Scheduler) RegisterDayMomentTask(name string, lastExecuted time.Time, offset time.Duration, hour, min, sec int, function interface{}, args ...interface{}) { + now := time.Now().Add(offset) + if IsMomentReached(now, lastExecuted, hour, min, sec) { + s.call(name, function, args...) + } + + moment := GetNextMoment(now, hour, min, sec) + s.RegisterRepeatedTask(name, moment.Sub(now), time.Hour*24, SchedulerForever, function, args...) +} + +func (s *Scheduler) task(name string, after, interval time.Duration, expr *cronexpr.Expression, times int, function interface{}, args ...interface{}) { + s.UnregisterTask(name) + + if expr == nil { + if after < s.tick { + after = s.tick + } + if interval < s.tick { + interval = s.tick + } + } + + var values = make([]reflect.Value, len(args)) + for i, v := range args { + values[i] = reflect.ValueOf(v) + } + + task := &schedulerTask{ + name: name, + after: after, + interval: interval, + total: times, + function: reflect.ValueOf(function), + args: values, + scheduler: s, + expr: expr, + } + var executor func(name string, caller func()) + if s.pool != nil { + executor = s.pool.getExecutor() + } + s.lock.Lock() + if s.executor != nil { + executor = s.pool.getExecutor() + } + + s.tasks[name] = task + if executor != nil { + task.timer = s.wheel.ScheduleFunc(task, func() { + executor(task.Name(), task.caller) + }) + } else { + task.timer = s.wheel.ScheduleFunc(task, task.caller) + } + s.lock.Unlock() +} + +func (s *Scheduler) call(name string, function any, args ...any) { + var values = make([]reflect.Value, len(args)) + for i, v := range args { + values[i] = reflect.ValueOf(v) + } + f := reflect.ValueOf(function) + s.lock.RLock() + defer s.lock.RUnlock() + if s.executor != nil { + s.executor(name, func() { + f.Call(values) + }) + } else { + f.Call(values) + } +} diff --git a/toolkit/chrono/scheduler_built_in.go b/toolkit/chrono/scheduler_built_in.go new file mode 100644 index 0000000..c494ae7 --- /dev/null +++ b/toolkit/chrono/scheduler_built_in.go @@ -0,0 +1,57 @@ +package chrono + +import "time" + +const ( + BuiltInSchedulerWheelSize = 50 +) + +var ( + buildInSchedulerPool *SchedulerPool + builtInScheduler *Scheduler +) + +func init() { + buildInSchedulerPool = NewDefaultSchedulerPool() + builtInScheduler = NewScheduler(DefaultSchedulerTick, BuiltInSchedulerWheelSize) +} + +// BuiltInSchedulerPool 获取内置的由 NewDefaultSchedulerPool 函数创建的时间调度器对象池 +func BuiltInSchedulerPool() *SchedulerPool { + return buildInSchedulerPool +} + +// BuiltInScheduler 获取内置的由 NewScheduler(DefaultSchedulerTick, BuiltInSchedulerWheelSize) 创建的时间调度器 +func BuiltInScheduler() *Scheduler { + return builtInScheduler +} + +// UnregisterTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.UnregisterTask 函数 +func UnregisterTask(name string) { + BuiltInScheduler().UnregisterTask(name) +} + +// RegisterCronTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.RegisterCronTask 函数 +func RegisterCronTask(name, expression string, function interface{}, args ...interface{}) error { + return BuiltInScheduler().RegisterCronTask(name, expression, function, args...) +} + +// RegisterImmediateCronTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.RegisterImmediateCronTask 函数 +func RegisterImmediateCronTask(name, expression string, function interface{}, args ...interface{}) error { + return BuiltInScheduler().RegisterImmediateCronTask(name, expression, function, args...) +} + +// RegisterAfterTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.RegisterAfterTask 函数 +func RegisterAfterTask(name string, after time.Duration, function interface{}, args ...interface{}) { + BuiltInScheduler().RegisterAfterTask(name, after, function, args...) +} + +// RegisterRepeatedTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.RegisterRepeatedTask 函数 +func RegisterRepeatedTask(name string, after, interval time.Duration, times int, function interface{}, args ...interface{}) { + BuiltInScheduler().RegisterRepeatedTask(name, after, interval, times, function, args...) +} + +// RegisterDayMomentTask 调用内置时间调度器 BuiltInScheduler 的 Scheduler.RegisterDayMomentTask 函数 +func RegisterDayMomentTask(name string, lastExecuted time.Time, offset time.Duration, hour, min, sec int, function interface{}, args ...interface{}) { + BuiltInScheduler().RegisterDayMomentTask(name, lastExecuted, offset, hour, min, sec, function, args...) +} diff --git a/toolkit/chrono/scheduler_pool.go b/toolkit/chrono/scheduler_pool.go new file mode 100644 index 0000000..ede53d4 --- /dev/null +++ b/toolkit/chrono/scheduler_pool.go @@ -0,0 +1,110 @@ +package chrono + +import ( + "fmt" + "sync" + "time" +) + +const ( + SchedulerPoolDefaultSize = 96 + SchedulerPoolDefaultTick = time.Millisecond * 10 + SchedulerPoolDefaultWheelSize = 10 +) + +// NewDefaultSchedulerPool 创建一个默认参数的并发安全的时间调度器对象池 +// - size: SchedulerPoolDefaultSize +// - tick: SchedulerPoolDefaultTick +// - wheelSize: SchedulerPoolDefaultWheelSize +func NewDefaultSchedulerPool() *SchedulerPool { + scheduler, err := NewSchedulerPool(SchedulerPoolDefaultSize, SchedulerPoolDefaultTick, SchedulerPoolDefaultWheelSize) + if err != nil { + panic(err) // 该错误不应该发生,用于在参数或实现变更后的提示 + } + return scheduler +} + +// NewSchedulerPool 创建一个并发安全的时间调度器对象池 +func NewSchedulerPool(size int, tick time.Duration, wheelSize int64) (*SchedulerPool, error) { + if size <= 0 { + return nil, fmt.Errorf("scheduler pool size must greater than 0, got: %d", size) + } + if wheelSize <= 0 { + return nil, fmt.Errorf("scheduler pool wheelSize must greater than 0, got: %d", size) + } + return &SchedulerPool{ + size: size, + tick: tick, + wheelSize: wheelSize, + generation: 1, + }, nil +} + +// SchedulerPool 并发安全的时间调度器对象池 +type SchedulerPool struct { + schedulers []*Scheduler // 池中维护的时间调度器 + lock sync.RWMutex // 用于并发安全的锁 + tick time.Duration // 时间周期 + wheelSize int64 // 时间轮尺寸 + size int // 池大小,控制了池中时间调度器的数量 + generation int64 // 池的代数 + executor func(name string, caller func()) // 任务执行器 +} + +// SetExecutor 设置该事件调度器对象池中整体的任务执行器 +func (p *SchedulerPool) SetExecutor(executor func(name string, caller func())) { + p.lock.Lock() + defer p.lock.Unlock() + p.executor = executor +} + +// SetSize 改变时间调度器对象池的大小,当传入的大小小于或等于 0 时,将会返回错误,并且不会发生任何改变 +// - 设置时间调度器对象池的大小可以在运行时动态调整,但是需要注意的是,调整后的大小不会影响已经产生的 Scheduler +// - 已经产生的 Scheduler 在被释放后将不会回到 SchedulerPool 中 +func (p *SchedulerPool) SetSize(size int) error { + if size <= 0 { + return fmt.Errorf("scheduler pool size must greater than 0, got: %d", size) + } + p.lock.Lock() + defer p.lock.Unlock() + p.size = size + return nil +} + +// Get 获取一个特定时间周期及时间轮尺寸的时间调度器,当池中存在可用的时间调度器时,将会直接返回,否则将会创建一个新的时间调度器 +func (p *SchedulerPool) Get() *Scheduler { + p.lock.Lock() + defer p.lock.Unlock() + var scheduler *Scheduler + if len(p.schedulers) > 0 { + scheduler = p.schedulers[0] + p.schedulers = p.schedulers[1:] + return scheduler + } + return newScheduler(p, p.tick, p.wheelSize) +} + +// Recycle 释放定时器池的资源并将其重置为全新的状态 +// - 执行该函数后,已有的时间调度器将会被停止,且不会重新加入到池中,一切都是新的开始 +func (p *SchedulerPool) Recycle() { + p.lock.Lock() + defer p.lock.Unlock() + for _, scheduler := range p.schedulers { + scheduler.wheel.Stop() + } + p.schedulers = nil + p.generation++ + return +} + +func (p *SchedulerPool) getGeneration() int64 { + p.lock.RLock() + defer p.lock.RUnlock() + return p.generation +} + +func (p *SchedulerPool) getExecutor() func(name string, caller func()) { + p.lock.RLock() + defer p.lock.RUnlock() + return p.executor +} diff --git a/toolkit/chrono/scheduler_task.go b/toolkit/chrono/scheduler_task.go new file mode 100644 index 0000000..d17fdd4 --- /dev/null +++ b/toolkit/chrono/scheduler_task.go @@ -0,0 +1,81 @@ +package chrono + +import ( + "github.com/RussellLuo/timingwheel" + "github.com/gorhill/cronexpr" + "reflect" + "sync" + "time" +) + +// schedulerTask 调度器 +type schedulerTask struct { + lock sync.RWMutex + scheduler *Scheduler // 任务所属的调度器 + timer *timingwheel.Timer // 任务执行定时器 + name string // 任务名称 + after time.Duration // 任务首次执行延迟 + interval time.Duration // 任务执行间隔 + function reflect.Value // 任务执行函数 + args []reflect.Value // 任务执行参数 + expr *cronexpr.Expression // 任务执行时间表达式 + + total int // 任务执行次数 + trigger int // 任务已执行次数 + kill bool // 任务是否已关闭 +} + +// Name 获取任务名称 +func (t *schedulerTask) Name() string { + return t.name +} + +// Next 获取任务下一次执行的时间 +func (t *schedulerTask) Next(prev time.Time) time.Time { + t.lock.RLock() + defer t.lock.RUnlock() + + if t.kill || (t.expr != nil && t.total > 0 && t.trigger > t.total) { + return time.Time{} + } + if t.expr != nil { + next := t.expr.Next(prev) + return next + } + if t.trigger == 0 { + t.trigger++ + return prev.Add(t.after) + } + t.trigger++ + return prev.Add(t.interval) +} + +func (t *schedulerTask) caller() { + t.lock.Lock() + + if t.kill { + t.lock.Unlock() + return + } + + if t.total > 0 && t.trigger > t.total { + t.lock.Unlock() + t.scheduler.UnregisterTask(t.name) + } else { + t.lock.Unlock() + } + t.function.Call(t.args) +} + +func (t *schedulerTask) close() { + t.lock.Lock() + defer t.lock.Unlock() + + if t.kill { + return + } + t.kill = true + if t.total <= 0 || t.trigger < t.total { + t.timer.Stop() + } +} diff --git a/toolkit/chrono/shceduler_test.go b/toolkit/chrono/shceduler_test.go new file mode 100644 index 0000000..9ecba82 --- /dev/null +++ b/toolkit/chrono/shceduler_test.go @@ -0,0 +1,14 @@ +package chrono_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/chrono" + "testing" + "time" +) + +func TestRegisterCronTask(t *testing.T) { + chrono.RegisterDayMomentTask("newday", time.Now().Add(time.Minute*-2), 0, 0, 0, 0, func() { + fmt.Println("newday") + }) +} diff --git a/toolkit/chrono/state_line.go b/toolkit/chrono/state_line.go new file mode 100644 index 0000000..e0377a4 --- /dev/null +++ b/toolkit/chrono/state_line.go @@ -0,0 +1,261 @@ +package chrono + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" + "github.com/kercylan98/minotaur/utils/generic" + "strings" + "time" +) + +// NewStateLine 创建一个从左向右由早到晚的状态时间线 +func NewStateLine[State generic.Basic](zero State) *StateLine[State] { + return &StateLine[State]{ + states: []State{zero}, + points: []time.Time{{}}, + trigger: [][]func(){{}}, + } +} + +// StateLine 表示一个状态时间线,它记录了一系列时间点及其对应的状态和触发器。 +// 在时间线中,每个时间点都与一个状态和一组触发器相关联,可以通过时间点查找状态,并触发与之相关联的触发器。 +type StateLine[State generic.Basic] struct { + states []State // 每个时间点对应的状态 + points []time.Time // 每个时间点 + trigger [][]func() // 每个时间点对应的触发器 +} + +// Check 根据状态顺序检查时间线是否合法 +// - missingAllowed: 是否允许状态缺失,如果为 true,则状态可以不连续,如果为 false,则状态必须连续 +// +// 状态不连续表示时间线中存在状态缺失,例如: +// - 状态为 [1, 2, 3, 4, 5] 的时间线,如果 missingAllowed 为 true,则状态为 [1, 3, 5] 也是合法的 +// - 状态为 [1, 2, 3, 4, 5] 的时间线,如果 missingAllowed 为 false,则状态为 [1, 3, 5] 是不合法的 +func (s *StateLine[State]) Check(missingAllowed bool, states ...State) bool { + var indexStored int + var indexInput int + + for indexStored < len(s.states) && indexInput < len(states) { + if s.states[indexStored] == states[indexInput] { + indexStored++ + indexInput++ + } else if missingAllowed { + indexInput++ + } else { + return false + } + } + + //如果存储序列还有剩余, 而输入序列已经遍历完 + if indexStored != len(s.states) && indexInput == len(states) { + return false + } + + // 如果输入序列还有剩余, 而存储序列已经遍历完 + if indexStored == len(s.states) && indexInput != len(states) && !missingAllowed { + return false + } + + return true +} + +// GetMissingStates 获取缺失的状态 +func (s *StateLine[State]) GetMissingStates(states ...State) []State { + var missing = make([]State, 0, len(states)) + for _, state := range states { + if !collection.InComparableSlice(s.states, state) { + missing = append(missing, state) + } + } + return missing +} + +// HasState 检查时间线中是否包含指定状态 +func (s *StateLine[State]) HasState(state State) bool { + return collection.InComparableSlice(s.states, state) +} + +// String 获取时间线的字符串表示 +func (s *StateLine[State]) String() string { + var parts []string + for i := 0; i < len(s.states); i++ { + parts = append(parts, fmt.Sprintf("[%v] %v", s.states[i], s.points[i])) + } + return strings.Join(parts, " > ") +} + +// AddState 添加一个状态到时间线中,状态不能与任一时间点重合,否则将被忽略 +// - onTrigger: 该状态绑定的触发器,该触发器不会被主动执行,需要主动获取触发器执行 +func (s *StateLine[State]) AddState(state State, t time.Time, onTrigger ...func()) *StateLine[State] { + if collection.InComparableSlice(s.states, state) { + return s + } + // 将 t 按照从左到右由早到晚的顺序插入到 points 中 + for i := 0; i < len(s.points); i++ { + if s.points[i].After(t) { + s.points = append(s.points[:i], append([]time.Time{t}, s.points[i:]...)...) + s.states = append(s.states[:i], append([]State{state}, s.states[i:]...)...) + s.trigger = append(s.trigger[:i], append([][]func(){onTrigger}, s.trigger[i:]...)...) + return s + } + } + s.points = append(s.points, t) + s.states = append(s.states, state) + s.trigger = append(s.trigger, onTrigger) + return s +} + +// GetTimeByState 获取指定状态的时间点 +func (s *StateLine[State]) GetTimeByState(state State) time.Time { + for i := 0; i < len(s.states); i++ { + if s.states[i] == state { + return s.points[i] + } + } + return time.Time{} +} + +// GetNextTimeByState 获取指定状态的下一个时间点 +func (s *StateLine[State]) GetNextTimeByState(state State) time.Time { + for i := 0; i < len(s.states); i++ { + if s.states[i] == state && i+1 < len(s.points) { + return s.points[i+1] + } + } + return s.points[0] +} + +// GetLastState 获取最后一个状态 +func (s *StateLine[State]) GetLastState() State { + return s.states[len(s.states)-1] +} + +// GetPrevTimeByState 获取指定状态的上一个时间点 +func (s *StateLine[State]) GetPrevTimeByState(state State) time.Time { + for i := len(s.states) - 1; i >= 0; i-- { + if s.states[i] == state && i > 0 { + return s.points[i-1] + } + } + return time.Time{} +} + +// GetIndexByState 获取指定状态的索引 +func (s *StateLine[State]) GetIndexByState(state State) int { + for i := 0; i < len(s.states); i++ { + if s.states[i] == state { + return i + } + } + return -1 +} + +// GetStateByTime 获取指定时间点的状态 +func (s *StateLine[State]) GetStateByTime(t time.Time) State { + for i := len(s.points) - 1; i >= 0; i-- { + point := s.points[i] + if point.Before(t) || point.Equal(t) { + return s.states[i] + } + } + return s.states[len(s.points)-1] +} + +// GetTimeByIndex 获取指定索引的时间点 +func (s *StateLine[State]) GetTimeByIndex(index int) time.Time { + return s.points[index] +} + +// Move 时间线整体移动 +func (s *StateLine[State]) Move(d time.Duration) *StateLine[State] { + for i := 0; i < len(s.points); i++ { + s.points[i] = s.points[i].Add(d) + } + return s +} + +// GetNextStateTimeByIndex 获取指定索引的下一个时间点 +func (s *StateLine[State]) GetNextStateTimeByIndex(index int) time.Time { + return s.points[index+1] +} + +// GetPrevStateTimeByIndex 获取指定索引的上一个时间点 +func (s *StateLine[State]) GetPrevStateTimeByIndex(index int) time.Time { + return s.points[index-1] +} + +// GetStateIndexByTime 获取指定时间点的索引 +func (s *StateLine[State]) GetStateIndexByTime(t time.Time) int { + for i := len(s.points) - 1; i >= 0; i-- { + var point = s.points[i] + if point.Before(t) || point.Equal(t) { + return i + } + } + return -1 +} + +// GetStateCount 获取状态数量 +func (s *StateLine[State]) GetStateCount() int { + return len(s.states) +} + +// GetStateByIndex 获取指定索引的状态 +func (s *StateLine[State]) GetStateByIndex(index int) State { + return s.states[index] +} + +// GetTriggerByTime 获取指定时间点的触发器 +func (s *StateLine[State]) GetTriggerByTime(t time.Time) []func() { + for i := len(s.points) - 1; i >= 0; i-- { + var point = s.points[i] + if point.Before(t) || point.Equal(t) { + return s.trigger[i] + } + } + return nil +} + +// GetTriggerByIndex 获取指定索引的触发器 +func (s *StateLine[State]) GetTriggerByIndex(index int) []func() { + return s.trigger[index] +} + +// GetTriggerByState 获取指定状态的触发器 +func (s *StateLine[State]) GetTriggerByState(state State) []func() { + for i := 0; i < len(s.states); i++ { + if s.states[i] == state { + return s.trigger[i] + } + } + return nil +} + +// AddTriggerToState 给指定状态添加触发器 +func (s *StateLine[State]) AddTriggerToState(state State, onTrigger ...func()) *StateLine[State] { + for i := 0; i < len(s.states); i++ { + if s.states[i] == state { + s.trigger[i] = append(s.trigger[i], onTrigger...) + return s + } + } + return s +} + +// Iterate 按照时间顺序遍历时间线 +func (s *StateLine[State]) Iterate(handler func(index int, state State, t time.Time) bool) { + for i := 0; i < len(s.points); i++ { + if !handler(i, s.states[i], s.points[i]) { + return + } + } +} + +// IterateReverse 按照时间逆序遍历时间线 +func (s *StateLine[State]) IterateReverse(handler func(index int, state State, t time.Time) bool) { + for i := len(s.points) - 1; i >= 0; i-- { + if !handler(i, s.states[i], s.points[i]) { + return + } + } +} diff --git a/toolkit/chrono/state_line_test.go b/toolkit/chrono/state_line_test.go new file mode 100644 index 0000000..df22895 --- /dev/null +++ b/toolkit/chrono/state_line_test.go @@ -0,0 +1,20 @@ +package chrono_test + +import ( + "github.com/kercylan98/minotaur/utils/chrono" + "testing" + "time" +) + +func TestNewStateLine(t *testing.T) { + sl := chrono.NewStateLine(0) + sl.AddState(1, time.Now()) + sl.AddState(2, time.Now().Add(-chrono.Hour)) + + sl.Iterate(func(index int, state int, ts time.Time) bool { + t.Log(index, state, ts) + return true + }) + + t.Log(sl.GetStateByTime(time.Now())) +} diff --git a/toolkit/collection/clone.go b/toolkit/collection/clone.go new file mode 100644 index 0000000..6f1e0ed --- /dev/null +++ b/toolkit/collection/clone.go @@ -0,0 +1,89 @@ +package collection + +import ( + "slices" +) + +// CloneSlice 通过创建一个新切片并将 slice 的元素复制到新切片的方式来克隆切片 +func CloneSlice[S ~[]V, V any](slice S) S { + return slices.Clone(slice) +} + +// CloneMap 通过创建一个新 map 并将 m 的元素复制到新 map 的方式来克隆 map +// - 当 m 为空时,将会返回 nil +func CloneMap[M ~map[K]V, K comparable, V any](m M) M { + if m == nil { + return nil + } + + var result = make(M, len(m)) + for k, v := range m { + result[k] = v + } + return result +} + +// CloneSliceN 通过创建一个新切片并将 slice 的元素复制到新切片的方式来克隆切片为 n 个切片 +// - 当 slice 为空时,将会返回 nil,当 n <= 0 时,将会返回零值切片 +func CloneSliceN[S ~[]V, V any](slice S, n int) []S { + if slice == nil { + return nil + } + if n <= 0 { + return []S{} + } + + var result = make([]S, n) + for i := 0; i < n; i++ { + result[i] = CloneSlice(slice) + } + return result +} + +// CloneMapN 通过创建一个新 map 并将 m 的元素复制到新 map 的方式来克隆 map 为 n 个 map +// - 当 m 为空时,将会返回 nil,当 n <= 0 时,将会返回零值切片 +func CloneMapN[M ~map[K]V, K comparable, V any](m M, n int) []M { + if m == nil { + return nil + } + + if n <= 0 { + return []M{} + } + + var result = make([]M, n) + for i := 0; i < n; i++ { + result[i] = CloneMap(m) + } + return result +} + +// CloneSlices 对 slices 中的每一项元素进行克隆,最终返回一个新的二维切片 +// - 当 slices 为空时,将会返回 nil +// - 该函数相当于使用 CloneSlice 函数一次性对多个切片进行克隆 +func CloneSlices[S ~[]V, V any](slices ...S) []S { + if slices == nil { + return nil + } + + var result = make([]S, len(slices)) + for i, slice := range slices { + result[i] = CloneSlice(slice) + } + return result +} + +// CloneMaps 对 maps 中的每一项元素进行克隆,最终返回一个新的 map 切片 +// - 当 maps 为空时,将会返回 nil +// - 该函数相当于使用 CloneMap 函数一次性对多个 map 进行克隆 +func CloneMaps[M ~map[K]V, K comparable, V any](maps ...M) []M { + if maps == nil { + return nil + } + + var result = make([]M, len(maps)) + for i, m := range maps { + result[i] = CloneMap(m) + } + return result +} diff --git a/toolkit/collection/clone_example_test.go b/toolkit/collection/clone_example_test.go new file mode 100644 index 0000000..7fe9b83 --- /dev/null +++ b/toolkit/collection/clone_example_test.go @@ -0,0 +1,70 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +// 在该示例中,将 slice 克隆后将会得到一个新的 slice result,而 result 和 slice 将不会有任何关联,但是如果 slice 中的元素是引用类型,那么 result 中的元素将会和 slice 中的元素指向同一个地址 +// - 示例中的结果将会输出 [1 2 3] +func ExampleCloneSlice() { + var slice = []int{1, 2, 3} + var result = collection.CloneSlice(slice) + fmt.Println(result) + // Output: + // [1 2 3] +} + +// 在该示例中,将 map 克隆后将会得到一个新的 map result,而 result 和 map 将不会有任何关联,但是如果 map 中的元素是引用类型,那么 result 中的元素将会和 map 中的元素指向同一个地址 +// - 示例中的结果将会输出 3 +func ExampleCloneMap() { + var m = map[int]int{1: 1, 2: 2, 3: 3} + var result = collection.CloneMap(m) + fmt.Println(len(result)) + // Output: + // 3 +} + +// 通过将 slice 克隆为 2 个新的 slice,将会得到一个新的 slice result,而 result 和 slice 将不会有任何关联,但是如果 slice 中的元素是引用类型,那么 result 中的元素将会和 slice 中的元素指向同一个地址 +// - result 的结果为 [[1 2 3] [1 2 3]] +// - 示例中的结果将会输出 2 +func ExampleCloneSliceN() { + var slice = []int{1, 2, 3} + var result = collection.CloneSliceN(slice, 2) + fmt.Println(len(result)) + // Output: + // 2 +} + +// 通过将 map 克隆为 2 个新的 map,将会得到一个新的 map result,而 result 和 map 将不会有任何关联,但是如果 map 中的元素是引用类型,那么 result 中的元素将会和 map 中的元素指向同一个地址 +// - result 的结果为 [map[1:1 2:2 3:3] map[1:1 2:2 3:3]] `无序的 Key-Value 对` +// - 示例中的结果将会输出 2 +func ExampleCloneMapN() { + var m = map[int]int{1: 1, 2: 2, 3: 3} + var result = collection.CloneMapN(m, 2) + fmt.Println(len(result)) + // Output: + // 2 +} + +// 通过将多个 slice 克隆为 2 个新的 slice,将会得到一个新的 slice result,而 result 和 slice 将不会有任何关联,但是如果 slice 中的元素是引用类型,那么 result 中的元素将会和 slice 中的元素指向同一个地址 +// - result 的结果为 [[1 2 3] [1 2 3]] +func ExampleCloneSlices() { + var slice1 = []int{1, 2, 3} + var slice2 = []int{1, 2, 3} + var result = collection.CloneSlices(slice1, slice2) + fmt.Println(len(result)) + // Output: + // 2 +} + +// 通过将多个 map 克隆为 2 个新的 map,将会得到一个新的 map result,而 result 和 map 将不会有任何关联,但是如果 map 中的元素是引用类型,那么 result 中的元素将会和 map 中的元素指向同一个地址 +// - result 的结果为 [map[1:1 2:2 3:3] map[1:1 2:2 3:3]] `无序的 Key-Value 对` +func ExampleCloneMaps() { + var m1 = map[int]int{1: 1, 2: 2, 3: 3} + var m2 = map[int]int{1: 1, 2: 2, 3: 3} + var result = collection.CloneMaps(m1, m2) + fmt.Println(len(result)) + // Output: + // 2 +} diff --git a/toolkit/collection/clone_test.go b/toolkit/collection/clone_test.go new file mode 100644 index 0000000..1fc2803 --- /dev/null +++ b/toolkit/collection/clone_test.go @@ -0,0 +1,190 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestCloneSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestCloneSlice_NonEmptySlice", []int{1, 2, 3}, []int{1, 2, 3}}, + {"TestCloneSlice_EmptySlice", []int{}, []int{}}, + {"TestCloneSlice_NilSlice", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneSlice(c.input) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + for i := 0; i < len(actual); i++ { + if actual[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the inputV of input is not equal") + } + } + }) + } +} + +func TestCloneMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]int + }{ + {"TestCloneMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]int{1: 1, 2: 2, 3: 3}}, + {"TestCloneMap_EmptyMap", map[int]int{}, map[int]int{}}, + {"TestCloneMap_NilMap", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneMap(c.input) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the inputV of map is not equal") + } + } + }) + } +} + +func TestCloneSliceN(t *testing.T) { + var cases = []struct { + name string + input []int + inputN int + expected [][]int + }{ + {"TestCloneSliceN_NonEmptySlice", []int{1, 2, 3}, 2, [][]int{{1, 2, 3}, {1, 2, 3}}}, + {"TestCloneSliceN_EmptySlice", []int{}, 2, [][]int{{}, {}}}, + {"TestCloneSliceN_NilSlice", nil, 2, nil}, + {"TestCloneSliceN_NonEmptySlice_ZeroN", []int{1, 2, 3}, 0, [][]int{}}, + {"TestCloneSliceN_EmptySlice_ZeroN", []int{}, 0, [][]int{}}, + {"TestCloneSliceN_NilSlice_ZeroN", nil, 0, nil}, + {"TestCloneSliceN_NonEmptySlice_NegativeN", []int{1, 2, 3}, -1, [][]int{}}, + {"TestCloneSliceN_EmptySlice_NegativeN", []int{}, -1, [][]int{}}, + {"TestCloneSliceN_NilSlice_NegativeN", nil, -1, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneSliceN(c.input, c.inputN) + if actual == nil { + if c.expected != nil { + t.Fatalf("%s failed, expected: %v, actual: %v, inputN: %d, error: %s", c.name, c.expected, c.inputN, actual, "after clone, the expected is nil") + } + return + } + for a, i := range actual { + for b, v := range i { + if v != c.expected[a][b] { + t.Fatalf("%s failed, expected: %v, actual: %v, inputN: %d, error: %s", c.name, c.expected, c.inputN, actual, "after clone, the inputV of input is not equal") + } + } + } + }) + } +} + +func TestCloneMapN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + inputN int + expected []map[int]int + }{ + {"TestCloneMapN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 2, []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 3: 3}}}, + {"TestCloneMapN_EmptyMap", map[int]int{}, 2, []map[int]int{{}, {}}}, + {"TestCloneMapN_NilMap", nil, 2, nil}, + {"TestCloneMapN_NonEmptyMap_ZeroN", map[int]int{1: 1, 2: 2, 3: 3}, 0, []map[int]int{}}, + {"TestCloneMapN_EmptyMap_ZeroN", map[int]int{}, 0, []map[int]int{}}, + {"TestCloneMapN_NilMap_ZeroN", nil, 0, nil}, + {"TestCloneMapN_NonEmptyMap_NegativeN", map[int]int{1: 1, 2: 2, 3: 3}, -1, []map[int]int{}}, + {"TestCloneMapN_EmptyMap_NegativeN", map[int]int{}, -1, []map[int]int{}}, + {"TestCloneMapN_NilMap_NegativeN", nil, -1, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneMapN(c.input, c.inputN) + if actual == nil { + if c.expected != nil { + t.Fatalf("%s failed, expected: %v, actual: %v, inputN: %d, error: %s", c.name, c.expected, actual, c.inputN, "after clone, the expected is nil") + } + return + } + for a, i := range actual { + for b, v := range i { + if v != c.expected[a][b] { + t.Fatalf("%s failed, expected: %v, actual: %v, inputN: %d, error: %s", c.name, c.expected, actual, c.inputN, "after clone, the inputV of map is not equal") + } + } + } + }) + } +} + +func TestCloneSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + expected [][]int + }{ + {"TestCloneSlices_NonEmptySlices", [][]int{{1, 2, 3}, {1, 2, 3}}, [][]int{{1, 2, 3}, {1, 2, 3}}}, + {"TestCloneSlices_EmptySlices", [][]int{{}, {}}, [][]int{{}, {}}}, + {"TestCloneSlices_NilSlices", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneSlices(c.input...) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + for a, i := range actual { + for b, v := range i { + if v != c.expected[a][b] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the inputV of input is not equal") + } + } + } + }) + } +} + +func TestCloneMaps(t *testing.T) { + var cases = []struct { + name string + input []map[int]int + expected []map[int]int + }{ + {"TestCloneMaps_NonEmptyMaps", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 3: 3}}, []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 3: 3}}}, + {"TestCloneMaps_EmptyMaps", []map[int]int{{}, {}}, []map[int]int{{}, {}}}, + {"TestCloneMaps_NilMaps", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.CloneMaps(c.input...) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of maps is not equal") + } + for a, i := range actual { + for b, v := range i { + if v != c.expected[a][b] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the inputV of maps is not equal") + } + } + } + }) + } +} diff --git a/toolkit/collection/collection.go b/toolkit/collection/collection.go new file mode 100644 index 0000000..519b27d --- /dev/null +++ b/toolkit/collection/collection.go @@ -0,0 +1,10 @@ +package collection + +import "github.com/kercylan98/minotaur/utils/generic" + +// ComparisonHandler 用于比较 `source` 和 `target` 两个值是否相同的比较函数 +// - 该函数接受两个参数,分别是源值和目标值,返回 true 的情况下即表示两者相同 +type ComparisonHandler[V any] func(source, target V) bool + +// OrderedValueGetter 用于获取 v 的可排序字段值的函数 +type OrderedValueGetter[V any, N generic.Ordered] func(v V) N diff --git a/toolkit/collection/contains.go b/toolkit/collection/contains.go new file mode 100644 index 0000000..dda4939 --- /dev/null +++ b/toolkit/collection/contains.go @@ -0,0 +1,385 @@ +package collection + +// EqualSlice 检查两个切片是否相等,当 handler 返回 true 时,表示 slice1 中的某个元素和 slice2 中的某个元素相匹配 +// - 当两个切片的容量不同时,不会影响最终的比较结果 +func EqualSlice[S ~[]V, V any](slice1 S, slice2 S, handler ComparisonHandler[V]) bool { + if len(slice1) != len(slice2) { + return false + } + for i, v1 := range slice1 { + if !handler(v1, slice2[i]) { + return false + } + } + return true +} + +// EqualComparableSlice 检查两个切片的值是否相同 +// - 当两个切片的容量不同时,不会影响最终的比较结果 +func EqualComparableSlice[S ~[]V, V comparable](slice1 S, slice2 S) bool { + if len(slice1) != len(slice2) { + return false + } + for i, v1 := range slice1 { + if v1 != slice2[i] { + return false + } + } + return true +} + +// EqualMap 检查两个 map 是否相等,当 handler 返回 true 时,表示 map1 中的某个元素和 map2 中的某个元素相匹配 +// - 当两个 map 的容量不同时,不会影响最终的比较结果 +func EqualMap[M ~map[K]V, K comparable, V any](map1 M, map2 M, handler ComparisonHandler[V]) bool { + if len(map1) != len(map2) { + return false + } + for k, v1 := range map1 { + if !handler(v1, map2[k]) { + return false + } + } + return true +} + +// EqualComparableMap 检查两个 map 的值是否相同 +// - 当两个 map 的容量不同时,不会影响最终的比较结果 +func EqualComparableMap[M ~map[K]V, K comparable, V comparable](map1 M, map2 M) bool { + if len(map1) != len(map2) { + return false + } + for k, v1 := range map1 { + if v1 != map2[k] { + return false + } + } + return true +} + +// InSlice 检查 v 是否被包含在 slice 中,当 handler 返回 true 时,表示 v 和 slice 中的某个元素相匹配 +func InSlice[S ~[]V, V any](slice S, v V, handler ComparisonHandler[V]) bool { + if len(slice) == 0 { + return false + } + for _, value := range slice { + if handler(v, value) { + return true + } + } + return false +} + +// InComparableSlice 检查 v 是否被包含在 slice 中 +func InComparableSlice[S ~[]V, V comparable](slice S, v V) bool { + if slice == nil { + return false + } + for _, value := range slice { + if value == v { + return true + } + } + return false +} + +// AllInSlice 检查 values 中的所有元素是否均被包含在 slice 中,当 handler 返回 true 时,表示 values 中的某个元素和 slice 中的某个元素相匹配 +// - 在所有 values 中的元素都被包含在 slice 中时,返回 true +// - 当 values 长度为 0 或为 nil 时,将返回 true +func AllInSlice[S ~[]V, V any](slice S, values []V, handler ComparisonHandler[V]) bool { + if len(slice) == 0 { + return false + } + for _, value := range values { + if !InSlice(slice, value, handler) { + return false + } + } + return true +} + +// AllInComparableSlice 检查 values 中的所有元素是否均被包含在 slice 中 +// - 在所有 values 中的元素都被包含在 slice 中时,返回 true +// - 当 values 长度为 0 或为 nil 时,将返回 true +func AllInComparableSlice[S ~[]V, V comparable](slice S, values []V) bool { + if len(slice) == 0 { + return false + } + for _, value := range values { + if !InComparableSlice(slice, value) { + return false + } + } + return true +} + +// AnyInSlice 检查 values 中的任意一个元素是否被包含在 slice 中,当 handler 返回 true 时,表示 value 中的某个元素和 slice 中的某个元素相匹配 +// - 当 values 中的任意一个元素被包含在 slice 中时,返回 true +func AnyInSlice[S ~[]V, V any](slice S, values []V, handler ComparisonHandler[V]) bool { + if len(slice) == 0 { + return false + } + for _, value := range values { + if InSlice(slice, value, handler) { + return true + } + } + return false +} + +// AnyInComparableSlice 检查 values 中的任意一个元素是否被包含在 slice 中 +// - 当 values 中的任意一个元素被包含在 slice 中时,返回 true +func AnyInComparableSlice[S ~[]V, V comparable](slice S, values []V) bool { + if len(slice) == 0 { + return false + } + for _, value := range values { + if InComparableSlice(slice, value) { + return true + } + } + return false +} + +// InSlices 通过将多个切片合并后检查 v 是否被包含在 slices 中,当 handler 返回 true 时,表示 v 和 slices 中的某个元素相匹配 +// - 当传入的 v 被包含在 slices 的任一成员中时,返回 true +func InSlices[S ~[]V, V any](slices []S, v V, handler ComparisonHandler[V]) bool { + return InSlice(MergeSlices(slices...), v, handler) +} + +// InComparableSlices 通过将多个切片合并后检查 v 是否被包含在 slices 中 +// - 当传入的 v 被包含在 slices 的任一成员中时,返回 true +func InComparableSlices[S ~[]V, V comparable](slices []S, v V) bool { + return InComparableSlice(MergeSlices(slices...), v) +} + +// AllInSlices 通过将多个切片合并后检查 values 中的所有元素是否被包含在 slices 中,当 handler 返回 true 时,表示 values 中的某个元素和 slices 中的某个元素相匹配 +// - 当 values 中的所有元素都被包含在 slices 的任一成员中时,返回 true +func AllInSlices[S ~[]V, V any](slices []S, values []V, handler ComparisonHandler[V]) bool { + return AllInSlice(MergeSlices(slices...), values, handler) +} + +// AllInComparableSlices 通过将多个切片合并后检查 values 中的所有元素是否被包含在 slices 中 +// - 当 values 中的所有元素都被包含在 slices 的任一成员中时,返回 true +func AllInComparableSlices[S ~[]V, V comparable](slices []S, values []V) bool { + return AllInComparableSlice(MergeSlices(slices...), values) +} + +// AnyInSlices 通过将多个切片合并后检查 values 中的任意一个元素是否被包含在 slices 中,当 handler 返回 true 时,表示 values 中的某个元素和 slices 中的某个元素相匹配 +// - 当 values 中的任意一个元素被包含在 slices 的任一成员中时,返回 true +func AnyInSlices[S ~[]V, V any](slices []S, values []V, handler ComparisonHandler[V]) bool { + return AnyInSlice(MergeSlices(slices...), values, handler) +} + +// AnyInComparableSlices 通过将多个切片合并后检查 values 中的任意一个元素是否被包含在 slices 中 +// - 当 values 中的任意一个元素被包含在 slices 的任一成员中时,返回 true +func AnyInComparableSlices[S ~[]V, V comparable](slices []S, values []V) bool { + return AnyInComparableSlice(MergeSlices(slices...), values) +} + +// InAllSlices 检查 v 是否被包含在 slices 的每一项元素中,当 handler 返回 true 时,表示 v 和 slices 中的某个元素相匹配 +// - 当 v 被包含在 slices 的每一项元素中时,返回 true +func InAllSlices[S ~[]V, V any](slices []S, v V, handler ComparisonHandler[V]) bool { + if len(slices) == 0 { + return false + } + for _, slice := range slices { + if !InSlice(slice, v, handler) { + return false + } + } + return true +} + +// InAllComparableSlices 检查 v 是否被包含在 slices 的每一项元素中 +// - 当 v 被包含在 slices 的每一项元素中时,返回 true +func InAllComparableSlices[S ~[]V, V comparable](slices []S, v V) bool { + if len(slices) == 0 { + return false + } + for _, slice := range slices { + if !InComparableSlice(slice, v) { + return false + } + } + return true +} + +// AnyInAllSlices 检查 slices 中的每一个元素是否均包含至少任意一个 values 中的元素,当 handler 返回 true 时,表示 value 中的某个元素和 slices 中的某个元素相匹配 +// - 当 slices 中的每一个元素均包含至少任意一个 values 中的元素时,返回 true +func AnyInAllSlices[S ~[]V, V any](slices []S, values []V, handler ComparisonHandler[V]) bool { + if len(slices) == 0 { + return false + } + for _, slice := range slices { + if !AnyInSlice(slice, values, handler) { + return false + } + } + return true +} + +// AnyInAllComparableSlices 检查 slices 中的每一个元素是否均包含至少任意一个 values 中的元素 +// - 当 slices 中的每一个元素均包含至少任意一个 values 中的元素时,返回 true +func AnyInAllComparableSlices[S ~[]V, V comparable](slices []S, values []V) bool { + if len(slices) == 0 { + return false + } + for _, slice := range slices { + if !AnyInComparableSlice(slice, values) { + return false + } + } + return true +} + +// KeyInMap 检查 m 中是否包含特定 key +func KeyInMap[M ~map[K]V, K comparable, V any](m M, key K) bool { + _, ok := m[key] + return ok +} + +// ValueInMap 检查 m 中是否包含特定 value,当 handler 返回 true 时,表示 value 和 m 中的某个元素相匹配 +func ValueInMap[M ~map[K]V, K comparable, V any](m M, value V, handler ComparisonHandler[V]) bool { + if len(m) == 0 { + return false + } + for _, v := range m { + if handler(value, v) { + return true + } + } + return false +} + +// AllKeyInMap 检查 m 中是否包含 keys 中所有的元素 +func AllKeyInMap[M ~map[K]V, K comparable, V any](m M, keys ...K) bool { + if len(m) < len(keys) { + return false + } + for _, key := range keys { + if !KeyInMap(m, key) { + return false + } + } + return true +} + +// AllValueInMap 检查 m 中是否包含 values 中所有的元素,当 handler 返回 true 时,表示 values 中的某个元素和 m 中的某个元素相匹配 +func AllValueInMap[M ~map[K]V, K comparable, V any](m M, values []V, handler ComparisonHandler[V]) bool { + if len(m) == 0 { + return false + } + for _, value := range values { + if !ValueInMap(m, value, handler) { + return false + } + } + return true +} + +// AnyKeyInMap 检查 m 中是否包含 keys 中任意一个元素 +func AnyKeyInMap[M ~map[K]V, K comparable, V any](m M, keys ...K) bool { + if len(m) == 0 { + return false + } + for _, key := range keys { + if KeyInMap(m, key) { + return true + } + } + return false +} + +// AnyValueInMap 检查 m 中是否包含 values 中任意一个元素,当 handler 返回 true 时,表示 values 中的某个元素和 m 中的某个元素相匹配 +func AnyValueInMap[M ~map[K]V, K comparable, V any](m M, values []V, handler ComparisonHandler[V]) bool { + if len(m) == 0 { + return false + } + for _, value := range values { + if ValueInMap(m, value, handler) { + return true + } + } + return false +} + +// AllKeyInMaps 检查 maps 中的每一个元素是否均包含 keys 中所有的元素 +func AllKeyInMaps[M ~map[K]V, K comparable, V any](maps []M, keys ...K) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if !AllKeyInMap(m, keys...) { + return false + } + } + return true +} + +// AllValueInMaps 检查 maps 中的每一个元素是否均包含 value 中所有的元素,当 handler 返回 true 时,表示 value 中的某个元素和 maps 中的某个元素相匹配 +func AllValueInMaps[M ~map[K]V, K comparable, V any](maps []M, values []V, handler ComparisonHandler[V]) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if !AllValueInMap(m, values, handler) { + return false + } + } + return true +} + +// AnyKeyInMaps 检查 keys 中的任意一个元素是否被包含在 maps 中的任意一个元素中 +// - 当 keys 中的任意一个元素被包含在 maps 中的任意一个元素中时,返回 true +func AnyKeyInMaps[M ~map[K]V, K comparable, V any](maps []M, keys ...K) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if AnyKeyInMap(m, keys...) { + return true + } + } + return false +} + +// AnyValueInMaps 检查 maps 中的任意一个元素是否包含 value 中的任意一个元素,当 handler 返回 true 时,表示 value 中的某个元素和 maps 中的某个元素相匹配 +// - 当 maps 中的任意一个元素包含 value 中的任意一个元素时,返回 true +func AnyValueInMaps[M ~map[K]V, K comparable, V any](maps []M, values []V, handler ComparisonHandler[V]) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if !AnyValueInMap(m, values, handler) { + return false + } + } + return true +} + +// KeyInAllMaps 检查 key 是否被包含在 maps 的每一个元素中 +func KeyInAllMaps[M ~map[K]V, K comparable, V any](maps []M, key K) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if !KeyInMap(m, key) { + return false + } + } + return true +} + +// AnyKeyInAllMaps 检查 maps 中的每一个元素是否均包含 keys 中任意一个元素 +// - 当 maps 中的每一个元素均包含 keys 中任意一个元素时,返回 true +func AnyKeyInAllMaps[M ~map[K]V, K comparable, V any](maps []M, keys []K) bool { + if len(maps) == 0 { + return false + } + for _, m := range maps { + if !AnyKeyInMap(m, keys...) { + return false + } + } + return true +} diff --git a/toolkit/collection/contains_example_test.go b/toolkit/collection/contains_example_test.go new file mode 100644 index 0000000..033f0fe --- /dev/null +++ b/toolkit/collection/contains_example_test.go @@ -0,0 +1,280 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleEqualSlice() { + s1 := []int{1, 2, 3} + s2 := []int{1} + s3 := []int{1, 2, 3} + fmt.Println(collection.EqualSlice(s1, s2, func(source, target int) bool { + return source == target + })) + fmt.Println(collection.EqualSlice(s1, s3, func(source, target int) bool { + return source == target + })) + // Output: + // false + // true +} + +func ExampleEqualComparableSlice() { + s1 := []int{1, 2, 3} + s2 := []int{1} + s3 := []int{1, 2, 3} + fmt.Println(collection.EqualComparableSlice(s1, s2)) + fmt.Println(collection.EqualComparableSlice(s1, s3)) + // Output: + // false + // true +} + +func ExampleEqualMap() { + m1 := map[string]int{"a": 1, "b": 2} + m2 := map[string]int{"a": 1} + m3 := map[string]int{"a": 1, "b": 2} + fmt.Println(collection.EqualMap(m1, m2, func(source, target int) bool { + return source == target + })) + fmt.Println(collection.EqualMap(m1, m3, func(source, target int) bool { + return source == target + })) + // Output: + // false + // true +} + +func ExampleEqualComparableMap() { + m1 := map[string]int{"a": 1, "b": 2} + m2 := map[string]int{"a": 1} + m3 := map[string]int{"a": 1, "b": 2} + fmt.Println(collection.EqualComparableMap(m1, m2)) + fmt.Println(collection.EqualComparableMap(m1, m3)) + // Output: + // false + // true +} + +func ExampleInSlice() { + result := collection.InSlice([]int{1, 2, 3}, 2, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleInComparableSlice() { + result := collection.InComparableSlice([]int{1, 2, 3}, 2) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllInSlice() { + result := collection.AllInSlice([]int{1, 2, 3}, []int{1, 2}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllInComparableSlice() { + result := collection.AllInComparableSlice([]int{1, 2, 3}, []int{1, 2}) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyInSlice() { + result := collection.AnyInSlice([]int{1, 2, 3}, []int{1, 2}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyInComparableSlice() { + result := collection.AnyInComparableSlice([]int{1, 2, 3}, []int{1, 2}) + fmt.Println(result) + // Output: + // true +} + +func ExampleInSlices() { + result := collection.InSlices([][]int{{1, 2, 3}, {4, 5, 6}}, 2, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleInComparableSlices() { + result := collection.InComparableSlices([][]int{{1, 2, 3}, {4, 5, 6}}, 2) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllInSlices() { + result := collection.AllInSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllInComparableSlices() { + result := collection.AllInComparableSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyInSlices() { + result := collection.AnyInSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyInComparableSlices() { + result := collection.AnyInComparableSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}) + fmt.Println(result) + // Output: + // true +} + +func ExampleInAllSlices() { + result := collection.InAllSlices([][]int{{1, 2, 3}, {4, 5, 6}}, 2, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // false +} + +func ExampleInAllComparableSlices() { + result := collection.InAllComparableSlices([][]int{{1, 2, 3}, {4, 5, 6}}, 2) + fmt.Println(result) + // Output: + // false +} + +func ExampleAnyInAllSlices() { + result := collection.AnyInAllSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // false +} + +func ExampleAnyInAllComparableSlices() { + result := collection.AnyInAllComparableSlices([][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2}) + fmt.Println(result) + // Output: + // false +} + +func ExampleKeyInMap() { + result := collection.KeyInMap(map[string]int{"a": 1, "b": 2}, "a") + fmt.Println(result) + // Output: + // true +} + +func ExampleValueInMap() { + result := collection.ValueInMap(map[string]int{"a": 1, "b": 2}, 2, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllKeyInMap() { + result := collection.AllKeyInMap(map[string]int{"a": 1, "b": 2}, "a", "b") + fmt.Println(result) + // Output: + // true +} + +func ExampleAllValueInMap() { + result := collection.AllValueInMap(map[string]int{"a": 1, "b": 2}, []int{1}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyKeyInMap() { + result := collection.AnyKeyInMap(map[string]int{"a": 1, "b": 2}, "a", "b") + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyValueInMap() { + result := collection.AnyValueInMap(map[string]int{"a": 1, "b": 2}, []int{1}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAllKeyInMaps() { + result := collection.AllKeyInMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, "a", "b") + fmt.Println(result) + // Output: + // true +} + +func ExampleAllValueInMaps() { + result := collection.AllValueInMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, []int{1}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyKeyInMaps() { + result := collection.AnyKeyInMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, "a", "b") + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyValueInMaps() { + result := collection.AnyValueInMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, []int{1}, func(source, target int) bool { + return source == target + }) + fmt.Println(result) + // Output: + // true +} + +func ExampleKeyInAllMaps() { + result := collection.KeyInAllMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, "a") + fmt.Println(result) + // Output: + // true +} + +func ExampleAnyKeyInAllMaps() { + result := collection.AnyKeyInAllMaps([]map[string]int{{"a": 1, "b": 2}, {"a": 1, "b": 2}}, []string{"a"}) + fmt.Println(result) + // Output: + // true +} diff --git a/toolkit/collection/contains_test.go b/toolkit/collection/contains_test.go new file mode 100644 index 0000000..f9f1b32 --- /dev/null +++ b/toolkit/collection/contains_test.go @@ -0,0 +1,809 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +var intComparisonHandler = func(source, target int) bool { + return source == target +} + +func TestEqualSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestEqualSlice_NonEmptySliceEqual", []int{1, 2, 3}, []int{1, 2, 3}, true}, + {"TestEqualSlice_NonEmptySliceNotEqual", []int{1, 2, 3}, []int{1, 2}, false}, + {"TestEqualSlice_EmptySlice", []int{}, []int{}, true}, + {"TestEqualSlice_NilSlice", nil, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.EqualSlice(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", + c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestEqualComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestEqualComparableSlice_NonEmptySliceEqual", []int{1, 2, 3}, []int{1, 2, 3}, true}, + {"TestEqualComparableSlice_NonEmptySliceNotEqual", []int{1, 2, 3}, []int{1, 2}, false}, + {"TestEqualComparableSlice_EmptySlice", []int{}, []int{}, true}, + {"TestEqualComparableSlice_NilSlice", nil, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.EqualComparableSlice(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", + c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestEqualMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + inputV map[int]int + expected bool + }{ + {"TestEqualMap_NonEmptyMapEqual", map[int]int{1: 1, 2: 2}, map[int]int{1: 1, 2: 2}, true}, + {"TestEqualMap_NonEmptyMapNotEqual", map[int]int{1: 1, 2: 2}, map[int]int{1: 1}, false}, + {"TestEqualMap_EmptyMap", map[int]int{}, map[int]int{}, true}, + {"TestEqualMap_NilMap", nil, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.EqualMap(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", + c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestEqualComparableMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + inputV map[int]int + expected bool + }{ + {"TestEqualComparableMap_NonEmptyMapEqual", map[int]int{1: 1, 2: 2}, map[int]int{1: 1, 2: 2}, true}, + {"TestEqualComparableMap_NonEmptyMapNotEqual", map[int]int{1: 1, 2: 2}, map[int]int{1: 1}, false}, + {"TestEqualComparableMap_EmptyMap", map[int]int{}, map[int]int{}, true}, + {"TestEqualComparableMap_NilMap", nil, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.EqualComparableMap(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", + c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV int + expected bool + }{ + {"TestInSlice_NonEmptySliceIn", []int{1, 2, 3}, 1, true}, + {"TestInSlice_NonEmptySliceNotIn", []int{1, 2, 3}, 4, false}, + {"TestInSlice_EmptySlice", []int{}, 1, false}, + {"TestInSlice_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InSlice(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV int + expected bool + }{ + {"TestInComparableSlice_NonEmptySliceIn", []int{1, 2, 3}, 1, true}, + {"TestInComparableSlice_NonEmptySliceNotIn", []int{1, 2, 3}, 4, false}, + {"TestInComparableSlice_EmptySlice", []int{}, 1, false}, + {"TestInComparableSlice_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InComparableSlice(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAllInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestAllInSlice_NonEmptySliceIn", []int{1, 2, 3}, []int{1, 2}, true}, + {"TestAllInSlice_NonEmptySliceNotIn", []int{1, 2, 3}, []int{1, 4}, false}, + {"TestAllInSlice_EmptySlice", []int{}, []int{1, 2}, false}, + {"TestAllInSlice_NilSlice", nil, []int{1, 2}, false}, + {"TestAllInSlice_EmptyValueSlice", []int{1, 2, 3}, []int{}, true}, + {"TestAllInSlice_NilValueSlice", []int{1, 2, 3}, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllInSlice(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAllInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestAllInComparableSlice_NonEmptySliceIn", []int{1, 2, 3}, []int{1, 2}, true}, + {"TestAllInComparableSlice_NonEmptySliceNotIn", []int{1, 2, 3}, []int{1, 4}, false}, + {"TestAllInComparableSlice_EmptySlice", []int{}, []int{1, 2}, false}, + {"TestAllInComparableSlice_NilSlice", nil, []int{1, 2}, false}, + {"TestAllInComparableSlice_EmptyValueSlice", []int{1, 2, 3}, []int{}, true}, + {"TestAllInComparableSlice_NilValueSlice", []int{1, 2, 3}, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllInComparableSlice(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAnyInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestAnyInSlice_NonEmptySliceIn", []int{1, 2, 3}, []int{1, 2}, true}, + {"TestAnyInSlice_NonEmptySliceNotIn", []int{1, 2, 3}, []int{1, 4}, true}, + {"TestAnyInSlice_EmptySlice", []int{}, []int{1, 2}, false}, + {"TestAnyInSlice_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInSlice_EmptyValueSlice", []int{1, 2, 3}, []int{}, false}, + {"TestAnyInSlice_NilValueSlice", []int{1, 2, 3}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInSlice(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAnyInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + inputV []int + expected bool + }{ + {"TestAnyInComparableSlice_NonEmptySliceIn", []int{1, 2, 3}, []int{1, 2}, true}, + {"TestAnyInComparableSlice_NonEmptySliceNotIn", []int{1, 2, 3}, []int{1, 4}, true}, + {"TestAnyInComparableSlice_EmptySlice", []int{}, []int{1, 2}, false}, + {"TestAnyInComparableSlice_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInComparableSlice_EmptyValueSlice", []int{1, 2, 3}, []int{}, false}, + {"TestAnyInComparableSlice_NilValueSlice", []int{1, 2, 3}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInComparableSlice(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestInSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + inputV int + expected bool + }{ + {"TestInSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, 1, true}, + {"TestInSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, 5, false}, + {"TestInSlices_EmptySlice", [][]int{{}, {}}, 1, false}, + {"TestInSlices_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InSlices(c.input, c.inputV, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestInComparableSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + inputV int + expected bool + }{ + {"TestInComparableSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, 1, true}, + {"TestInComparableSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, 5, false}, + {"TestInComparableSlices_EmptySlice", [][]int{{}, {}}, 1, false}, + {"TestInComparableSlices_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InComparableSlices(c.input, c.inputV) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "") + } + }) + } +} + +func TestAllInSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + inputValues []int + expected bool + }{ + {"TestAllInSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, []int{1, 2}, true}, + {"TestAllInSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, false}, + {"TestAllInSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAllInSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAllInSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, true}, + {"TestAllInSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllInSlices(c.input, c.inputValues, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAllInComparableSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + inputValues []int + expected bool + }{ + {"TestAllInComparableSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, []int{1, 2}, true}, + {"TestAllInComparableSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, false}, + {"TestAllInComparableSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAllInComparableSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAllInComparableSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, true}, + {"TestAllInComparableSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllInComparableSlices(c.input, c.inputValues) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "") + } + }) + } +} + +func TestAnyInSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + values []int + expected bool + }{ + {"TestAnyInSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, []int{1, 2}, true}, + {"TestAnyInSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, true}, + {"TestAnyInSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyInSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, false}, + {"TestAnyInSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInSlices(c.slices, c.values, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAnyInComparableSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + values []int + expected bool + }{ + {"TestAnyInComparableSlices_NonEmptySliceIn", [][]int{{1, 2}, {3, 4}}, []int{1, 2}, true}, + {"TestAnyInComparableSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, true}, + {"TestAnyInComparableSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyInComparableSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInComparableSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, false}, + {"TestAnyInComparableSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInComparableSlices(c.slices, c.values) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "") + } + }) + } +} + +func TestInAllSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + value int + expected bool + }{ + {"TestInAllSlices_NonEmptySliceIn", [][]int{{1, 2}, {1, 3}}, 1, true}, + {"TestInAllSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, 5, false}, + {"TestInAllSlices_EmptySlice", [][]int{{}, {}}, 1, false}, + {"TestInAllSlices_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InAllSlices(c.slices, c.value, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestInAllComparableSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + value int + expected bool + }{ + {"TestInAllComparableSlices_NonEmptySliceIn", [][]int{{1, 2}, {1, 3}}, 1, true}, + {"TestInAllComparableSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, 5, false}, + {"TestInAllComparableSlices_EmptySlice", [][]int{{}, {}}, 1, false}, + {"TestInAllComparableSlices_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.InAllComparableSlices(c.slices, c.value) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "") + } + }) + } +} + +func TestAnyInAllSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + values []int + expected bool + }{ + {"TestAnyInAllSlices_NonEmptySliceIn", [][]int{{1, 2}, {1, 3}}, []int{1, 2}, true}, + {"TestAnyInAllSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, false}, + {"TestAnyInAllSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyInAllSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInAllSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, false}, + {"TestAnyInAllSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInAllSlices(c.slices, c.values, func(source, target int) bool { + return source == target + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAnyInAllComparableSlices(t *testing.T) { + var cases = []struct { + name string + slices [][]int + values []int + expected bool + }{ + {"TestAnyInAllComparableSlices_NonEmptySliceIn", [][]int{{1, 2}, {1, 3}}, []int{1, 2}, true}, + {"TestAnyInAllComparableSlices_NonEmptySliceNotIn", [][]int{{1, 2}, {3, 4}}, []int{1, 5}, false}, + {"TestAnyInAllComparableSlices_EmptySlice", [][]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyInAllComparableSlices_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyInAllComparableSlices_EmptyValueSlice", [][]int{{1, 2}, {3, 4}}, []int{}, false}, + {"TestAnyInAllComparableSlices_NilValueSlice", [][]int{{1, 2}, {3, 4}}, nil, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyInAllComparableSlices(c.slices, c.values) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "") + } + }) + } +} + +func TestKeyInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + key int + expected bool + }{ + {"TestKeyInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, 1, true}, + {"TestKeyInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, 3, false}, + {"TestKeyInMap_EmptySlice", map[int]int{}, 1, false}, + {"TestKeyInMap_NilSlice", nil, 1, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.KeyInMap(c.m, c.key) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestValueInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + value int + handler collection.ComparisonHandler[int] + expected bool + }{ + {"TestValueInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, 1, intComparisonHandler, true}, + {"TestValueInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, 3, intComparisonHandler, false}, + {"TestValueInMap_EmptySlice", map[int]int{}, 1, intComparisonHandler, false}, + {"TestValueInMap_NilSlice", nil, 1, intComparisonHandler, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.ValueInMap(c.m, c.value, c.handler) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAllKeyInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + keys []int + expected bool + }{ + {"TestAllKeyInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, []int{1, 2}, true}, + {"TestAllKeyInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, []int{1, 3}, false}, + {"TestAllKeyInMap_EmptySlice", map[int]int{}, []int{1, 2}, false}, + {"TestAllKeyInMap_NilSlice", nil, []int{1, 2}, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllKeyInMap(c.m, c.keys...) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAllValueInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + values []int + expected bool + }{ + {"TestAllValueInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, []int{1, 2}, true}, + {"TestAllValueInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, []int{1, 3}, false}, + {"TestAllValueInMap_EmptySlice", map[int]int{}, []int{1, 2}, false}, + {"TestAllValueInMap_NilSlice", nil, []int{1, 2}, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllValueInMap(c.m, c.values, intComparisonHandler) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after clone, the length of input is not equal") + } + }) + } +} + +func TestAnyKeyInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + keys []int + expected bool + }{ + {"TestAnyKeyInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, []int{1, 2}, true}, + {"TestAnyKeyInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, []int{1, 3}, true}, + {"TestAnyKeyInMap_EmptySlice", map[int]int{}, []int{1, 2}, false}, + {"TestAnyKeyInMap_NilSlice", nil, []int{1, 2}, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyKeyInMap(c.m, c.keys...) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAnyValueInMap(t *testing.T) { + var cases = []struct { + name string + m map[int]int + values []int + expected bool + }{ + {"TestAnyValueInMap_NonEmptySliceIn", map[int]int{1: 1, 2: 2}, []int{1, 2}, true}, + {"TestAnyValueInMap_NonEmptySliceNotIn", map[int]int{1: 1, 2: 2}, []int{1, 3}, true}, + {"TestAnyValueInMap_EmptySlice", map[int]int{}, []int{1, 2}, false}, + {"TestAnyValueInMap_NilSlice", nil, []int{1, 2}, false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyValueInMap(c.m, c.values, intComparisonHandler) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAllKeyInMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + keys []int + expected bool + }{ + {"TestAllKeyInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 2}, false}, + {"TestAllKeyInMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 3}, false}, + {"TestAllKeyInMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, []int{1, 2}, false}, + {"TestAllKeyInMaps_NilSlice", []map[int]int{{}, {}}, []int{1, 2}, false}, + {"TestAllKeyInMaps_EmptySlice", []map[int]int{}, []int{1, 2}, false}, + {"TestAllKeyInMaps_NilSlice", nil, []int{1, 2}, false}, + {"TestAllKeyInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, []int{1, 2}, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllKeyInMaps(c.maps, c.keys...) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAllValueInMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + values []int + expected bool + }{ + {"TestAllValueInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 2}, false}, + {"TestAllValueInMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 3}, false}, + {"TestAllValueInMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, []int{1, 2}, false}, + {"TestAllValueInMaps_NilSlice", []map[int]int{{}, {}}, []int{1, 2}, false}, + {"TestAllValueInMaps_EmptySlice", []map[int]int{}, []int{1, 2}, false}, + {"TestAllValueInMaps_NilSlice", nil, []int{1, 2}, false}, + {"TestAllValueInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, []int{1, 2}, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AllValueInMaps(c.maps, c.values, intComparisonHandler) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAnyKeyInMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + keys []int + expected bool + }{ + {"TestAnyKeyInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 2}, true}, + {"TestAnyKeyInMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 3}, true}, + {"TestAnyKeyInMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, []int{1, 2}, true}, + {"TestAnyKeyInMaps_NilSlice", []map[int]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyKeyInMaps_EmptySlice", []map[int]int{}, []int{1, 2}, false}, + {"TestAnyKeyInMaps_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyKeyInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, []int{1, 2}, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyKeyInMaps(c.maps, c.keys...) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAnyValueInMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + values []int + expected bool + }{ + {"TestAnyValueInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 2}, false}, + {"TestAnyValueInMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 3}, true}, + {"TestAnyValueInMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, []int{1, 2}, false}, + {"TestAnyValueInMaps_NilSlice", []map[int]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyValueInMaps_EmptySlice", []map[int]int{}, []int{1, 2}, false}, + {"TestAnyValueInMaps_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyValueInMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, []int{1, 2}, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyValueInMaps(c.maps, c.values, intComparisonHandler) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestKeyInAllMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + key int + expected bool + }{ + {"TestKeyInAllMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, 1, false}, + {"TestKeyInAllMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, 3, false}, + {"TestKeyInAllMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, 1, false}, + {"TestKeyInAllMaps_NilSlice", []map[int]int{{}, {}}, 1, false}, + {"TestKeyInAllMaps_EmptySlice", []map[int]int{}, 1, false}, + {"TestKeyInAllMaps_NilSlice", nil, 1, false}, + {"TestKeyInAllMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, 1, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.KeyInAllMaps(c.maps, c.key) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "not as expected") + } + }) + } +} + +func TestAnyKeyInAllMaps(t *testing.T) { + var cases = []struct { + name string + maps []map[int]int + keys []int + expected bool + }{ + {"TestAnyKeyInAllMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 2}, false}, + {"TestAnyKeyInAllMaps_NonEmptySliceNotIn", []map[int]int{{1: 1, 2: 2}, {3: 3, 4: 4}}, []int{1, 3}, true}, + {"TestAnyKeyInAllMaps_EmptySlice", []map[int]int{{1: 1, 2: 2}, {}}, []int{1, 2}, false}, + {"TestAnyKeyInAllMaps_NilSlice", []map[int]int{{}, {}}, []int{1, 2}, false}, + {"TestAnyKeyInAllMaps_EmptySlice", []map[int]int{}, []int{1, 2}, false}, + {"TestAnyKeyInAllMaps_NilSlice", nil, []int{1, 2}, false}, + {"TestAnyKeyInAllMaps_NonEmptySliceIn", []map[int]int{{1: 1, 2: 2, 3: 3}, {1: 1, 2: 2, 4: 4}}, []int{1, 2}, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual = collection.AnyKeyInAllMaps(c.maps, c.keys) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, actual) + } + }) + } +} diff --git a/toolkit/collection/convert.go b/toolkit/collection/convert.go new file mode 100644 index 0000000..e5e7f54 --- /dev/null +++ b/toolkit/collection/convert.go @@ -0,0 +1,171 @@ +package collection + +// ConvertSliceToBatches 将切片 s 转换为分批次的切片,当 batchSize 小于等于 0 或者 s 长度为 0 时,将会返回 nil +func ConvertSliceToBatches[S ~[]V, V any](s S, batchSize int) []S { + if len(s) == 0 || batchSize <= 0 { + return nil + } + var batches = make([]S, 0, len(s)/batchSize+1) + for i := 0; i < len(s); i += batchSize { + var end = i + batchSize + if end > len(s) { + end = len(s) + } + batches = append(batches, s[i:end]) + } + return batches +} + +// ConvertMapKeysToBatches 将映射的键转换为分批次的切片,当 batchSize 小于等于 0 或者 m 长度为 0 时,将会返回 nil +func ConvertMapKeysToBatches[M ~map[K]V, K comparable, V any](m M, batchSize int) [][]K { + if len(m) == 0 || batchSize <= 0 { + return nil + } + var batches = make([][]K, 0, len(m)/batchSize+1) + var keys = ConvertMapKeysToSlice(m) + for i := 0; i < len(keys); i += batchSize { + var end = i + batchSize + if end > len(keys) { + end = len(keys) + } + batches = append(batches, keys[i:end]) + } + return batches +} + +// ConvertMapValuesToBatches 将映射的值转换为分批次的切片,当 batchSize 小于等于 0 或者 m 长度为 0 时,将会返回 nil +func ConvertMapValuesToBatches[M ~map[K]V, K comparable, V any](m M, batchSize int) [][]V { + if len(m) == 0 || batchSize <= 0 { + return nil + } + var batches = make([][]V, 0, len(m)/batchSize+1) + var values = ConvertMapValuesToSlice(m) + for i := 0; i < len(values); i += batchSize { + var end = i + batchSize + if end > len(values) { + end = len(values) + } + batches = append(batches, values[i:end]) + } + return batches +} + +// ConvertSliceToAny 将切片转换为任意类型的切片 +func ConvertSliceToAny[S ~[]V, V any](s S) []any { + if len(s) == 0 { + return nil + } + var r = make([]any, len(s)) + for i, v := range s { + r[i] = v + } + return r +} + +// ConvertSliceToIndexMap 将切片转换为索引为键的映射 +func ConvertSliceToIndexMap[S ~[]V, V any](s S) map[int]V { + if len(s) == 0 { + return make(map[int]V) + } + var r = make(map[int]V, len(s)) + for i, v := range s { + r[i] = v + } + return r +} + +// ConvertSliceToIndexOnlyMap 将切片转换为索引为键的映射 +func ConvertSliceToIndexOnlyMap[S ~[]V, V any](s S) map[int]struct{} { + if len(s) == 0 { + return nil + } + var r = make(map[int]struct{}, len(s)) + for i := range s { + r[i] = struct{}{} + } + return r +} + +// ConvertSliceToMap 将切片转换为值为键的映射 +func ConvertSliceToMap[S ~[]V, V comparable](s S) map[V]struct{} { + if len(s) == 0 { + return nil + } + var r = make(map[V]struct{}, len(s)) + for _, v := range s { + r[v] = struct{}{} + } + return r +} + +// ConvertSliceToBoolMap 将切片转换为值为键的映射 +func ConvertSliceToBoolMap[S ~[]V, V comparable](s S) map[V]bool { + if len(s) == 0 { + return make(map[V]bool) + } + var r = make(map[V]bool, len(s)) + for _, v := range s { + r[v] = true + } + return r +} + +// ConvertMapKeysToSlice 将映射的键转换为切片 +func ConvertMapKeysToSlice[M ~map[K]V, K comparable, V any](m M) []K { + if len(m) == 0 { + return nil + } + var r = make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} + +// ConvertMapValuesToSlice 将映射的值转换为切片 +func ConvertMapValuesToSlice[M ~map[K]V, K comparable, V any](m M) []V { + if len(m) == 0 { + return nil + } + var r = make([]V, 0, len(m)) + for _, v := range m { + r = append(r, v) + } + return r +} + +// InvertMap 将映射的键和值互换 +func InvertMap[M ~map[K]V, N map[V]K, K, V comparable](m M) N { + if m == nil { + return nil + } + var r = make(N, len(m)) + for k, v := range m { + r[v] = k + } + return r +} + +// ConvertMapValuesToBool 将映射的值转换为布尔值 +func ConvertMapValuesToBool[M ~map[K]V, N map[K]bool, K comparable, V any](m M) N { + if m == nil { + return nil + } + var r = make(N, len(m)) + for k := range m { + r[k] = true + } + return r +} + +// ReverseSlice 将切片反转 +func ReverseSlice[S ~[]V, V any](s *S) { + if s == nil { + return + } + + var length = len(*s) + for i := 0; i < length/2; i++ { + (*s)[i], (*s)[length-i-1] = (*s)[length-i-1], (*s)[i] + } +} diff --git a/toolkit/collection/convert_example_test.go b/toolkit/collection/convert_example_test.go new file mode 100644 index 0000000..468e6ea --- /dev/null +++ b/toolkit/collection/convert_example_test.go @@ -0,0 +1,130 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" + "reflect" + "sort" +) + +func ExampleConvertSliceToBatches() { + result := collection.ConvertSliceToBatches([]int{1, 2, 3}, 2) + for _, v := range result { + fmt.Println(v) + } + // Output: + // [1 2] + // [3] +} + +func ExampleConvertMapKeysToBatches() { + result := collection.ConvertMapKeysToBatches(map[int]int{1: 1, 2: 2, 3: 3}, 2) + fmt.Println(len(result)) + // Output: + // 2 +} + +func ExampleConvertMapValuesToBatches() { + result := collection.ConvertMapValuesToBatches(map[int]int{1: 1, 2: 2, 3: 3}, 2) + fmt.Println(len(result)) + // Output: + // 2 +} + +func ExampleConvertSliceToAny() { + result := collection.ConvertSliceToAny([]int{1, 2, 3}) + fmt.Println(reflect.TypeOf(result).String(), len(result)) + // Output: + // []interface {} 3 +} + +func ExampleConvertSliceToIndexMap() { + slice := []int{1, 2, 3} + result := collection.ConvertSliceToIndexMap(slice) + for i, v := range slice { + fmt.Println(result[i], v) + } + // Output: + // 1 1 + // 2 2 + // 3 3 +} + +func ExampleConvertSliceToIndexOnlyMap() { + slice := []int{1, 2, 3} + result := collection.ConvertSliceToIndexOnlyMap(slice) + expected := map[int]bool{0: true, 1: true, 2: true} + for k := range result { + fmt.Println(expected[k]) + } + // Output: + // true + // true + // true +} + +func ExampleConvertSliceToMap() { + slice := []int{1, 2, 3} + result := collection.ConvertSliceToMap(slice) + fmt.Println(collection.AllKeyInMap(result, slice...)) + // Output: + // true +} + +func ExampleConvertSliceToBoolMap() { + slice := []int{1, 2, 3} + result := collection.ConvertSliceToBoolMap(slice) + for _, v := range slice { + fmt.Println(v, result[v]) + } + // Output: + // 1 true + // 2 true + // 3 true +} + +func ExampleConvertMapKeysToSlice() { + result := collection.ConvertMapKeysToSlice(map[int]int{1: 1, 2: 2, 3: 3}) + sort.Ints(result) + for i, v := range result { + fmt.Println(i, v) + } + // Output: + // 0 1 + // 1 2 + // 2 3 +} + +func ExampleConvertMapValuesToSlice() { + result := collection.ConvertMapValuesToSlice(map[int]int{1: 1, 2: 2, 3: 3}) + expected := map[int]bool{1: true, 2: true, 3: true} + for _, v := range result { + fmt.Println(expected[v]) + } + // Output: + // true + // true + // true +} + +func ExampleInvertMap() { + result := collection.InvertMap(map[int]string{1: "a", 2: "b", 3: "c"}) + fmt.Println(collection.AllKeyInMap(result, "a", "b", "c")) + // Output: + // true +} + +func ExampleConvertMapValuesToBool() { + result := collection.ConvertMapValuesToBool(map[int]int{1: 1}) + fmt.Println(result) + // Output: + // map[1:true] +} + +func ExampleReverseSlice() { + var s = []int{1, 2, 3} + collection.ReverseSlice(&s) + fmt.Println(s) + // Output: + // [3 2 1] +} diff --git a/toolkit/collection/convert_test.go b/toolkit/collection/convert_test.go new file mode 100644 index 0000000..a117cc9 --- /dev/null +++ b/toolkit/collection/convert_test.go @@ -0,0 +1,377 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "reflect" + "testing" +) + +func TestConvertSliceToBatches(t *testing.T) { + var cases = []struct { + name string + input []int + batch int + expected [][]int + }{ + {name: "TestConvertSliceToBatches_NonEmpty", input: []int{1, 2, 3}, batch: 2, expected: [][]int{{1, 2}, {3}}}, + {name: "TestConvertSliceToBatches_Empty", input: []int{}, batch: 2, expected: nil}, + {name: "TestConvertSliceToBatches_Nil", input: nil, batch: 2, expected: nil}, + {name: "TestConvertSliceToBatches_NonPositive", input: []int{1, 2, 3}, batch: 0, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToBatches(c.input, c.batch) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for i := 0; i < len(actual); i++ { + av, ev := actual[i], c.expected[i] + if len(av) != len(ev) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for j := 0; j < len(av); j++ { + aj, ej := av[j], ev[j] + if reflect.TypeOf(aj).Kind() != reflect.TypeOf(ej).Kind() { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + if aj != ej { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + } + }) + } +} + +func TestConvertMapKeysToBatches(t *testing.T) { + var cases = []struct { + name string + input map[int]int + batch int + expected [][]int + }{ + {name: "TestConvertMapKeysToBatches_NonEmpty", input: map[int]int{1: 1, 2: 2, 3: 3}, batch: 2, expected: [][]int{{1, 2}, {3}}}, + {name: "TestConvertMapKeysToBatches_Empty", input: map[int]int{}, batch: 2, expected: nil}, + {name: "TestConvertMapKeysToBatches_Nil", input: nil, batch: 2, expected: nil}, + {name: "TestConvertMapKeysToBatches_NonPositive", input: map[int]int{1: 1, 2: 2, 3: 3}, batch: 0, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertMapKeysToBatches(c.input, c.batch) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + }) + } +} + +func TestConvertMapValuesToBatches(t *testing.T) { + var cases = []struct { + name string + input map[int]int + batch int + expected [][]int + }{ + {name: "TestConvertMapValuesToBatches_NonEmpty", input: map[int]int{1: 1, 2: 2, 3: 3}, batch: 2, expected: [][]int{{1, 2}, {3}}}, + {name: "TestConvertMapValuesToBatches_Empty", input: map[int]int{}, batch: 2, expected: nil}, + {name: "TestConvertMapValuesToBatches_Nil", input: nil, batch: 2, expected: nil}, + {name: "TestConvertMapValuesToBatches_NonPositive", input: map[int]int{1: 1, 2: 2, 3: 3}, batch: 0, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertMapValuesToBatches(c.input, c.batch) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + }) + } +} + +func TestConvertSliceToAny(t *testing.T) { + var cases = []struct { + name string + input []int + expected []interface{} + }{ + {name: "TestConvertSliceToAny_NonEmpty", input: []int{1, 2, 3}, expected: []any{1, 2, 3}}, + {name: "TestConvertSliceToAny_Empty", input: []int{}, expected: []any{}}, + {name: "TestConvertSliceToAny_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToAny(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for i := 0; i < len(actual); i++ { + av, ev := actual[i], c.expected[i] + if reflect.TypeOf(av).Kind() != reflect.TypeOf(ev).Kind() { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertSliceToIndexMap(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]int + }{ + {name: "TestConvertSliceToIndexMap_NonEmpty", input: []int{1, 2, 3}, expected: map[int]int{0: 1, 1: 2, 2: 3}}, + {name: "TestConvertSliceToIndexMap_Empty", input: []int{}, expected: map[int]int{}}, + {name: "TestConvertSliceToIndexMap_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToIndexMap(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if c.expected[k] != v { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertSliceToIndexOnlyMap(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]struct{} + }{ + {name: "TestConvertSliceToIndexOnlyMap_NonEmpty", input: []int{1, 2, 3}, expected: map[int]struct{}{0: {}, 1: {}, 2: {}}}, + {name: "TestConvertSliceToIndexOnlyMap_Empty", input: []int{}, expected: map[int]struct{}{}}, + {name: "TestConvertSliceToIndexOnlyMap_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToIndexOnlyMap(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if _, ok := c.expected[k]; !ok { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + if v != struct{}{} { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertSliceToMap(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]struct{} + }{ + {name: "TestConvertSliceToMap_NonEmpty", input: []int{1, 2, 3}, expected: map[int]struct{}{1: {}, 2: {}, 3: {}}}, + {name: "TestConvertSliceToMap_Empty", input: []int{}, expected: map[int]struct{}{}}, + {name: "TestConvertSliceToMap_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToMap(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if _, ok := c.expected[k]; !ok { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + if v != struct{}{} { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertSliceToBoolMap(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]bool + }{ + {name: "TestConvertSliceToBoolMap_NonEmpty", input: []int{1, 2, 3}, expected: map[int]bool{1: true, 2: true, 3: true}}, + {name: "TestConvertSliceToBoolMap_Empty", input: []int{}, expected: map[int]bool{}}, + {name: "TestConvertSliceToBoolMap_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertSliceToBoolMap(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if c.expected[k] != v { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertMapKeysToSlice(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected []int + }{ + {name: "TestConvertMapKeysToSlice_NonEmpty", input: map[int]int{1: 1, 2: 2, 3: 3}, expected: []int{1, 2, 3}}, + {name: "TestConvertMapKeysToSlice_Empty", input: map[int]int{}, expected: []int{}}, + {name: "TestConvertMapKeysToSlice_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertMapKeysToSlice(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + var matchCount = 0 + for _, av := range actual { + for _, ev := range c.expected { + if av == ev { + matchCount++ + } + } + } + if matchCount != len(actual) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + }) + } +} + +func TestConvertMapValuesToSlice(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected []int + }{ + {name: "TestConvertMapValuesToSlice_NonEmpty", input: map[int]int{1: 1, 2: 2, 3: 3}, expected: []int{1, 2, 3}}, + {name: "TestConvertMapValuesToSlice_Empty", input: map[int]int{}, expected: []int{}}, + {name: "TestConvertMapValuesToSlice_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertMapValuesToSlice(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + var matchCount = 0 + for _, av := range actual { + for _, ev := range c.expected { + if av == ev { + matchCount++ + } + } + } + if matchCount != len(actual) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + }) + } +} + +func TestInvertMap(t *testing.T) { + var cases = []struct { + name string + input map[int]string + expected map[string]int + }{ + {name: "TestInvertMap_NonEmpty", input: map[int]string{1: "1", 2: "2", 3: "3"}, expected: map[string]int{"1": 1, "2": 2, "3": 3}}, + {name: "TestInvertMap_Empty", input: map[int]string{}, expected: map[string]int{}}, + {name: "TestInvertMap_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.InvertMap[map[int]string, map[string]int](c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if c.expected[k] != v { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestConvertMapValuesToBool(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {name: "TestConvertMapValuesToBool_NonEmpty", input: map[int]int{1: 1, 2: 2, 3: 3}, expected: map[int]bool{1: true, 2: true, 3: true}}, + {name: "TestConvertMapValuesToBool_Empty", input: map[int]int{}, expected: map[int]bool{}}, + {name: "TestConvertMapValuesToBool_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ConvertMapValuesToBool[map[int]int, map[int]bool](c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for k, v := range actual { + if c.expected[k] != v { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestReverseSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {name: "TestReverseSlice_NonEmpty", input: []int{1, 2, 3}, expected: []int{3, 2, 1}}, + {name: "TestReverseSlice_Empty", input: []int{}, expected: []int{}}, + {name: "TestReverseSlice_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.ReverseSlice(&c.input) + if len(c.input) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + for i := 0; i < len(c.input); i++ { + av, ev := c.input[i], c.expected[i] + if reflect.TypeOf(av).Kind() != reflect.TypeOf(ev).Kind() { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + } + }) + } +} diff --git a/toolkit/collection/doc.go b/toolkit/collection/doc.go new file mode 100644 index 0000000..195bdbb --- /dev/null +++ b/toolkit/collection/doc.go @@ -0,0 +1,2 @@ +// Package collection 定义了各种对于集合操作有用的各种函数 +package collection diff --git a/toolkit/collection/drop.go b/toolkit/collection/drop.go new file mode 100644 index 0000000..d6c1e49 --- /dev/null +++ b/toolkit/collection/drop.go @@ -0,0 +1,82 @@ +package collection + +// ClearSlice 清空切片 +func ClearSlice[S ~[]V, V any](slice *S) { + if slice == nil { + return + } + *slice = (*slice)[0:0] +} + +// ClearMap 清空 map +func ClearMap[M ~map[K]V, K comparable, V any](m M) { + for k := range m { + delete(m, k) + } +} + +// DropSliceByIndices 删除切片中特定索引的元素 +func DropSliceByIndices[S ~[]V, V any](slice *S, indices ...int) { + if slice == nil { + return + } + if len(indices) == 0 { + return + } + + excludeMap := make(map[int]bool) + for _, ex := range indices { + excludeMap[ex] = true + } + + validElements := (*slice)[:0] + for i, v := range *slice { + if !excludeMap[i] { + validElements = append(validElements, v) + } + } + + *slice = validElements +} + +// DropSliceByCondition 删除切片中符合条件的元素 +// - condition 的返回值为 true 时,将会删除该元素 +func DropSliceByCondition[S ~[]V, V any](slice *S, condition func(v V) bool) { + if slice == nil { + return + } + if condition == nil { + return + } + + validElements := (*slice)[:0] + for _, v := range *slice { + if !condition(v) { + validElements = append(validElements, v) + } + } + + *slice = validElements +} + +// DropSliceOverlappingElements 删除切片中与另一个切片重叠的元素 +func DropSliceOverlappingElements[S ~[]V, V any](slice *S, anotherSlice []V, comparisonHandler ComparisonHandler[V]) { + if slice == nil { + return + } + if anotherSlice == nil { + return + } + if comparisonHandler == nil { + return + } + + validElements := (*slice)[:0] + for _, v := range *slice { + if !InSlice(anotherSlice, v, comparisonHandler) { + validElements = append(validElements, v) + } + } + + *slice = validElements +} diff --git a/toolkit/collection/drop_example_test.go b/toolkit/collection/drop_example_test.go new file mode 100644 index 0000000..490e7c5 --- /dev/null +++ b/toolkit/collection/drop_example_test.go @@ -0,0 +1,47 @@ +package collection + +import "fmt" + +func ExampleClearSlice() { + slice := []int{1, 2, 3, 4, 5} + ClearSlice(&slice) + fmt.Println(slice) + // Output: + // [] +} + +func ExampleClearMap() { + m := map[int]int{1: 1, 2: 2, 3: 3} + ClearMap(m) + fmt.Println(m) + // Output: + // map[] +} + +func ExampleDropSliceByIndices() { + slice := []int{1, 2, 3, 4, 5} + DropSliceByIndices(&slice, 1, 3) + fmt.Println(slice) + // Output: + // [1 3 5] +} + +func ExampleDropSliceByCondition() { + slice := []int{1, 2, 3, 4, 5} + DropSliceByCondition(&slice, func(v int) bool { + return v%2 == 0 + }) + fmt.Println(slice) + // Output: + // [1 3 5] +} + +func ExampleDropSliceOverlappingElements() { + slice := []int{1, 2, 3, 4, 5} + DropSliceOverlappingElements(&slice, []int{1, 3, 5}, func(source, target int) bool { + return source == target + }) + fmt.Println(slice) + // Output: + // [2 4] +} diff --git a/toolkit/collection/drop_test.go b/toolkit/collection/drop_test.go new file mode 100644 index 0000000..b4540e9 --- /dev/null +++ b/toolkit/collection/drop_test.go @@ -0,0 +1,145 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestClearSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestClearSlice_NonEmptySlice", []int{1, 2, 3}, []int{}}, + {"TestClearSlice_EmptySlice", []int{}, []int{}}, + {"TestClearSlice_NilSlice", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.ClearSlice(&c.input) + if len(c.input) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after clear, the length of input is not equal") + } + for i := 0; i < len(c.input); i++ { + if c.input[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after clear, the inputV of input is not equal") + } + } + }) + } +} + +func TestClearMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]int + }{ + {"TestClearMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]int{}}, + {"TestClearMap_EmptyMap", map[int]int{}, map[int]int{}}, + {"TestClearMap_NilMap", nil, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.ClearMap(c.input) + if len(c.input) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after clear, the length of map is not equal") + } + for k, v := range c.input { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after clear, the inputV of map is not equal") + } + } + }) + } +} + +func TestDropSliceByIndices(t *testing.T) { + var cases = []struct { + name string + input []int + indices []int + expected []int + }{ + {"TestDropSliceByIndices_NonEmptySlice", []int{1, 2, 3, 4, 5}, []int{1, 3}, []int{1, 3, 5}}, + {"TestDropSliceByIndices_EmptySlice", []int{}, []int{1, 3}, []int{}}, + {"TestDropSliceByIndices_NilSlice", nil, []int{1, 3}, nil}, + {"TestDropSliceByIndices_NonEmptySlice", []int{1, 2, 3, 4, 5}, []int{}, []int{1, 2, 3, 4, 5}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.DropSliceByIndices(&c.input, c.indices...) + if len(c.input) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the length of input is not equal") + } + for i := 0; i < len(c.input); i++ { + if c.input[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the inputV of input is not equal") + } + } + }) + } +} + +func TestDropSliceByCondition(t *testing.T) { + var cases = []struct { + name string + input []int + condition func(v int) bool + expected []int + }{ + {"TestDropSliceByCondition_NonEmptySlice", []int{1, 2, 3, 4, 5}, func(v int) bool { return v%2 == 0 }, []int{1, 3, 5}}, + {"TestDropSliceByCondition_EmptySlice", []int{}, func(v int) bool { return v%2 == 0 }, []int{}}, + {"TestDropSliceByCondition_NilSlice", nil, func(v int) bool { return v%2 == 0 }, nil}, + {"TestDropSliceByCondition_NonEmptySlice", []int{1, 2, 3, 4, 5}, func(v int) bool { return v%2 == 1 }, []int{2, 4}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.DropSliceByCondition(&c.input, c.condition) + if len(c.input) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the length of input is not equal") + } + for i := 0; i < len(c.input); i++ { + if c.input[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the inputV of input is not equal") + } + } + }) + } +} + +func TestDropSliceOverlappingElements(t *testing.T) { + var cases = []struct { + name string + input []int + anotherSlice []int + comparisonHandler collection.ComparisonHandler[int] + expected []int + expectedAnother []int + expectedComparison []int + }{ + {"TestDropSliceOverlappingElements_NonEmptySlice", []int{1, 2, 3, 4, 5}, []int{3, 4, 5, 6, 7}, func(v1, v2 int) bool { return v1 == v2 }, []int{1, 2}, []int{6, 7}, []int{3, 4, 5}}, + {"TestDropSliceOverlappingElements_EmptySlice", []int{}, []int{3, 4, 5, 6, 7}, func(v1, v2 int) bool { return v1 == v2 }, []int{}, []int{3, 4, 5, 6, 7}, []int{}}, + {"TestDropSliceOverlappingElements_NilSlice", nil, []int{3, 4, 5, 6, 7}, func(v1, v2 int) bool { return v1 == v2 }, nil, []int{3, 4, 5, 6, 7}, nil}, + {"TestDropSliceOverlappingElements_NonEmptySlice", []int{1, 2, 3, 4, 5}, []int{}, func(v1, v2 int) bool { return v1 == v2 }, []int{1, 2, 3, 4, 5}, []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.DropSliceOverlappingElements(&c.input, c.anotherSlice, c.comparisonHandler) + if len(c.input) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the length of input is not equal") + } + for i := 0; i < len(c.input); i++ { + if c.input[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, c.input, "after drop, the inputV of input is not equal") + } + } + }) + } +} diff --git a/toolkit/collection/duplicate.go b/toolkit/collection/duplicate.go new file mode 100644 index 0000000..5f6907c --- /dev/null +++ b/toolkit/collection/duplicate.go @@ -0,0 +1,83 @@ +package collection + +// DeduplicateSliceInPlace 去除切片中的重复元素 +func DeduplicateSliceInPlace[S ~[]V, V comparable](s *S) { + if s == nil || len(*s) < 2 { + return + } + + var m = make(map[V]struct{}, len(*s)) + var writeIndex int + for readIndex, v := range *s { + if _, ok := m[v]; !ok { + (*s)[writeIndex] = (*s)[readIndex] + writeIndex++ + m[v] = struct{}{} + } + } + *s = (*s)[:writeIndex] +} + +// DeduplicateSlice 去除切片中的重复元素,返回新切片 +func DeduplicateSlice[S ~[]V, V comparable](s S) S { + if s == nil || len(s) < 2 { + return any(s).(S) + } + + var r = make([]V, 0, len(s)) + var m = make(map[V]struct{}, len(s)) + for _, v := range s { + if _, ok := m[v]; !ok { + r = append(r, v) + m[v] = struct{}{} + } + } + return r +} + +// DeduplicateSliceInPlaceWithCompare 去除切片中的重复元素,使用自定义的比较函数 +func DeduplicateSliceInPlaceWithCompare[S ~[]V, V any](s *S, compare func(a, b V) bool) { + if s == nil || len(*s) < 2 { + return + } + seen := make(map[int]struct{}) + resultIndex := 0 + for i := range *s { + unique := true + for j := range seen { + if compare((*s)[i], (*s)[j]) { + unique = false // Found a duplicate + break + } + } + if unique { + seen[i] = struct{}{} + (*s)[resultIndex] = (*s)[i] + resultIndex++ + } + } + *s = (*s)[:resultIndex] +} + +// DeduplicateSliceWithCompare 去除切片中的重复元素,使用自定义的比较函数,返回新的切片 +func DeduplicateSliceWithCompare[S ~[]V, V any](s S, compare func(a, b V) bool) S { + if s == nil || compare == nil || len(s) < 2 { + return s + } + seen := make(map[int]struct{}) + var result = make([]V, 0, len(s)) + for i := range s { + unique := true + for j := range result { + if compare(s[i], result[j]) { + unique = false + break + } + } + if unique { + result = append(result, s[i]) + seen[i] = struct{}{} + } + } + return result +} diff --git a/toolkit/collection/duplicate_example_test.go b/toolkit/collection/duplicate_example_test.go new file mode 100644 index 0000000..026903c --- /dev/null +++ b/toolkit/collection/duplicate_example_test.go @@ -0,0 +1,37 @@ +package collection + +import "fmt" + +func ExampleDeduplicateSliceInPlace() { + slice := []int{1, 2, 3, 4, 5, 5, 4, 3, 2, 1} + DeduplicateSliceInPlace(&slice) + fmt.Println(slice) + // Output: + // [1 2 3 4 5] +} + +func ExampleDeduplicateSlice() { + slice := []int{1, 2, 3, 4, 5, 5, 4, 3, 2, 1} + fmt.Println(DeduplicateSlice(slice)) + // Output: + // [1 2 3 4 5] +} + +func ExampleDeduplicateSliceInPlaceWithCompare() { + slice := []int{1, 2, 3, 4, 5, 5, 4, 3, 2, 1} + DeduplicateSliceInPlaceWithCompare(&slice, func(a, b int) bool { + return a == b + }) + fmt.Println(slice) + // Output: + // [1 2 3 4 5] +} + +func ExampleDeduplicateSliceWithCompare() { + slice := []int{1, 2, 3, 4, 5, 5, 4, 3, 2, 1} + fmt.Println(DeduplicateSliceWithCompare(slice, func(a, b int) bool { + return a == b + })) + // Output: + // [1 2 3 4 5] +} diff --git a/toolkit/collection/duplicate_test.go b/toolkit/collection/duplicate_test.go new file mode 100644 index 0000000..2632441 --- /dev/null +++ b/toolkit/collection/duplicate_test.go @@ -0,0 +1,118 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestDeduplicateSliceInPlace(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {name: "TestDeduplicateSliceInPlace_NonEmpty", input: []int{1, 2, 3, 1, 2, 3}, expected: []int{1, 2, 3}}, + {name: "TestDeduplicateSliceInPlace_Empty", input: []int{}, expected: []int{}}, + {name: "TestDeduplicateSliceInPlace_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.DeduplicateSliceInPlace(&c.input) + if len(c.input) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + for i := 0; i < len(c.input); i++ { + av, ev := c.input[i], c.expected[i] + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + } + }) + } +} + +func TestDeduplicateSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {name: "TestDeduplicateSlice_NonEmpty", input: []int{1, 2, 3, 1, 2, 3}, expected: []int{1, 2, 3}}, + {name: "TestDeduplicateSlice_Empty", input: []int{}, expected: []int{}}, + {name: "TestDeduplicateSlice_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.DeduplicateSlice(c.input) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for i := 0; i < len(actual); i++ { + av, ev := actual[i], c.expected[i] + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} + +func TestDeduplicateSliceInPlaceWithCompare(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {name: "TestDeduplicateSliceInPlaceWithCompare_NonEmpty", input: []int{1, 2, 3, 1, 2, 3}, expected: []int{1, 2, 3}}, + {name: "TestDeduplicateSliceInPlaceWithCompare_Empty", input: []int{}, expected: []int{}}, + {name: "TestDeduplicateSliceInPlaceWithCompare_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.DeduplicateSliceInPlaceWithCompare(&c.input, func(a, b int) bool { + return a == b + }) + if len(c.input) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + for i := 0; i < len(c.input); i++ { + av, ev := c.input[i], c.expected[i] + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, c.input) + } + } + }) + } +} + +func TestDeduplicateSliceWithCompare(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {name: "TestDeduplicateSliceWithCompare_NonEmpty", input: []int{1, 2, 3, 1, 2, 3}, expected: []int{1, 2, 3}}, + {name: "TestDeduplicateSliceWithCompare_Empty", input: []int{}, expected: []int{}}, + {name: "TestDeduplicateSliceWithCompare_Nil", input: nil, expected: nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.DeduplicateSliceWithCompare(c.input, func(a, b int) bool { + return a == b + }) + if len(actual) != len(c.expected) { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + for i := 0; i < len(actual); i++ { + av, ev := actual[i], c.expected[i] + if av != ev { + t.Errorf("expected: %v, actual: %v", c.expected, actual) + } + } + }) + } +} diff --git a/toolkit/collection/filter.go b/toolkit/collection/filter.go new file mode 100644 index 0000000..57ceab7 --- /dev/null +++ b/toolkit/collection/filter.go @@ -0,0 +1,140 @@ +package collection + +// FilterOutByIndices 过滤切片中特定索引的元素,返回过滤后的切片 +func FilterOutByIndices[S []V, V any](slice S, indices ...int) S { + if slice == nil || len(slice) == 0 || len(indices) == 0 { + return slice + } + + excludeMap := make(map[int]bool) + for _, ex := range indices { + if ex >= 0 && ex < len(slice) { + excludeMap[ex] = true + } + } + + if len(excludeMap) == 0 { + return slice + } + + validElements := make([]V, 0, len(slice)-len(excludeMap)) + for i, v := range slice { + if !excludeMap[i] { + validElements = append(validElements, v) + } + } + + return validElements +} + +// FilterOutByCondition 过滤切片中符合条件的元素,返回过滤后的切片 +// - condition 的返回值为 true 时,将会过滤掉该元素 +func FilterOutByCondition[S ~[]V, V any](slice S, condition func(v V) bool) S { + if slice == nil { + return nil + } + if condition == nil { + return slice + } + + validElements := make([]V, 0, len(slice)) + for _, v := range slice { + if !condition(v) { + validElements = append(validElements, v) + } + } + + return validElements +} + +// FilterOutByKey 过滤 map 中特定的 key,返回过滤后的 map +func FilterOutByKey[M ~map[K]V, K comparable, V any](m M, key K) M { + if m == nil { + return nil + } + + validMap := make(M, len(m)-1) + for k, v := range m { + if k != key { + validMap[k] = v + } + } + + return validMap +} + +// FilterOutByValue 过滤 map 中特定的 value,返回过滤后的 map +func FilterOutByValue[M ~map[K]V, K comparable, V any](m M, value V, handler ComparisonHandler[V]) M { + if m == nil { + return nil + } + + validMap := make(M, len(m)) + for k, v := range m { + if !handler(value, v) { + validMap[k] = v + } + } + + return validMap +} + +// FilterOutByKeys 过滤 map 中多个 key,返回过滤后的 map +func FilterOutByKeys[M ~map[K]V, K comparable, V any](m M, keys ...K) M { + if m == nil { + return nil + } + if len(keys) == 0 { + return m + } + + validMap := make(M, len(m)-len(keys)) + for k, v := range m { + if !InSlice(keys, k, func(source, target K) bool { + return source == target + }) { + validMap[k] = v + } + } + + return validMap +} + +// FilterOutByValues 过滤 map 中多个 values,返回过滤后的 map +func FilterOutByValues[M ~map[K]V, K comparable, V any](m M, values []V, handler ComparisonHandler[V]) M { + if m == nil { + return nil + } + if len(values) == 0 { + return m + } + + validMap := make(M, len(m)) + for k, v := range m { + if !InSlice(values, v, handler) { + validMap[k] = v + } + } + + return validMap +} + +// FilterOutByMap 过滤 map 中符合条件的元素,返回过滤后的 map +// - condition 的返回值为 true 时,将会过滤掉该元素 +func FilterOutByMap[M ~map[K]V, K comparable, V any](m M, condition func(k K, v V) bool) M { + if m == nil { + return nil + } + if condition == nil { + return m + } + + validMap := make(M, len(m)) + for k, v := range m { + if !condition(k, v) { + validMap[k] = v + } + } + + return validMap +} diff --git a/toolkit/collection/filter_example_test.go b/toolkit/collection/filter_example_test.go new file mode 100644 index 0000000..cd80fac --- /dev/null +++ b/toolkit/collection/filter_example_test.go @@ -0,0 +1,74 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleFilterOutByIndices() { + var slice = []int{1, 2, 3, 4, 5} + var result = collection.FilterOutByIndices(slice, 1, 3) + fmt.Println(result) + // Output: + // [1 3 5] +} + +func ExampleFilterOutByCondition() { + var slice = []int{1, 2, 3, 4, 5} + var result = collection.FilterOutByCondition(slice, func(v int) bool { + return v%2 == 0 + }) + fmt.Println(result) + // Output: + // [1 3 5] +} + +func ExampleFilterOutByKey() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result = collection.FilterOutByKey(m, "b") + fmt.Println(result) + // Output: + // map[a:1 c:3] +} + +func ExampleFilterOutByValue() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result = collection.FilterOutByValue(m, 2, func(source, target int) bool { + return source == target + }) + fmt.Println(len(result)) + // Output: + // 2 +} + +func ExampleFilterOutByKeys() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result = collection.FilterOutByKeys(m, "a", "c") + fmt.Println(result) + // Output: + // map[b:2] +} + +func ExampleFilterOutByValues() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result = collection.FilterOutByValues(m, []int{1}, func(source, target int) bool { + return source == target + }) + for i, s := range []string{"a", "b", "c"} { + fmt.Println(i, result[s]) + } + // Output: + // 0 0 + // 1 2 + // 2 3 +} + +func ExampleFilterOutByMap() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result = collection.FilterOutByMap(m, func(k string, v int) bool { + return k == "a" || v == 3 + }) + fmt.Println(result) + // Output: + // map[b:2] +} diff --git a/toolkit/collection/filter_test.go b/toolkit/collection/filter_test.go new file mode 100644 index 0000000..edc1d55 --- /dev/null +++ b/toolkit/collection/filter_test.go @@ -0,0 +1,207 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestFilterOutByIndices(t *testing.T) { + var cases = []struct { + name string + input []int + indices []int + expected []int + }{ + {"TestFilterOutByIndices_NonEmptySlice", []int{1, 2, 3, 4, 5}, []int{1, 3}, []int{1, 3, 5}}, + {"TestFilterOutByIndices_EmptySlice", []int{}, []int{1, 3}, []int{}}, + {"TestFilterOutByIndices_NilSlice", nil, []int{1, 3}, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByIndices(c.input, c.indices...) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of input is not equal") + } + for i := 0; i < len(actual); i++ { + if actual[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of input is not equal") + } + } + }) + } +} + +func TestFilterOutByCondition(t *testing.T) { + var cases = []struct { + name string + input []int + condition func(int) bool + expected []int + }{ + {"TestFilterOutByCondition_NonEmptySlice", []int{1, 2, 3, 4, 5}, func(v int) bool { + return v%2 == 0 + }, []int{1, 3, 5}}, + {"TestFilterOutByCondition_EmptySlice", []int{}, func(v int) bool { + return v%2 == 0 + }, []int{}}, + {"TestFilterOutByCondition_NilSlice", nil, func(v int) bool { + return v%2 == 0 + }, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByCondition(c.input, c.condition) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of input is not equal") + } + for i := 0; i < len(actual); i++ { + if actual[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of input is not equal") + } + } + }) + } +} + +func TestFilterOutByKey(t *testing.T) { + var cases = []struct { + name string + input map[int]int + key int + expected map[int]int + }{ + {"TestFilterOutByKey_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1, map[int]int{2: 2, 3: 3}}, + {"TestFilterOutByKey_EmptyMap", map[int]int{}, 1, map[int]int{}}, + {"TestFilterOutByKey_NilMap", nil, 1, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByKey(c.input, c.key) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of map is not equal") + } + } + }) + } +} + +func TestFilterOutByValue(t *testing.T) { + var cases = []struct { + name string + input map[int]int + value int + expected map[int]int + }{ + {"TestFilterOutByValue_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1, map[int]int{2: 2, 3: 3}}, + {"TestFilterOutByValue_EmptyMap", map[int]int{}, 1, map[int]int{}}, + {"TestFilterOutByValue_NilMap", nil, 1, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByValue(c.input, c.value, func(source, target int) bool { + return source == target + }) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of map is not equal") + } + } + }) + } +} + +func TestFilterOutByKeys(t *testing.T) { + var cases = []struct { + name string + input map[int]int + keys []int + expected map[int]int + }{ + {"TestFilterOutByKeys_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, []int{1, 3}, map[int]int{2: 2}}, + {"TestFilterOutByKeys_EmptyMap", map[int]int{}, []int{1, 3}, map[int]int{}}, + {"TestFilterOutByKeys_NilMap", nil, []int{1, 3}, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByKeys(c.input, c.keys...) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of map is not equal") + } + } + }) + } +} + +func TestFilterOutByValues(t *testing.T) { + var cases = []struct { + name string + input map[int]int + values []int + expected map[int]int + }{ + {"TestFilterOutByValues_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, []int{1, 3}, map[int]int{2: 2}}, + {"TestFilterOutByValues_EmptyMap", map[int]int{}, []int{1, 3}, map[int]int{}}, + {"TestFilterOutByValues_NilMap", nil, []int{1, 3}, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByValues(c.input, c.values, func(source, target int) bool { + return source == target + }) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of map is not equal") + } + } + }) + } +} + +func TestFilterOutByMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + filter map[int]int + expected map[int]int + }{ + {"TestFilterOutByMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]int{1: 1, 3: 3}, map[int]int{2: 2}}, + {"TestFilterOutByMap_EmptyMap", map[int]int{}, map[int]int{1: 1, 3: 3}, map[int]int{}}, + {"TestFilterOutByMap_NilMap", nil, map[int]int{1: 1, 3: 3}, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FilterOutByMap(c.input, func(k int, v int) bool { + return c.filter[k] == v + }) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the length of map is not equal") + } + for k, v := range actual { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "after filter, the inputV of map is not equal") + } + } + }) + } +} diff --git a/toolkit/collection/find.go b/toolkit/collection/find.go new file mode 100644 index 0000000..c773615 --- /dev/null +++ b/toolkit/collection/find.go @@ -0,0 +1,348 @@ +package collection + +import ( + "github.com/kercylan98/minotaur/utils/generic" +) + +// FindLoopedNextInSlice 返回 i 的下一个数组成员,当 i 达到数组长度时从 0 开始 +// - 当 i 为负数时将返回第一个元素 +func FindLoopedNextInSlice[S ~[]V, V any](slice S, i int) (next int, value V) { + if i < 0 { + return 0, slice[0] + } + next = i + 1 + if next == len(slice) { + next = 0 + } + return next, slice[next] +} + +// FindLoopedPrevInSlice 返回 i 的上一个数组成员,当 i 为 0 时从数组末尾开始 +// - 当 i 为负数时将返回最后一个元素 +func FindLoopedPrevInSlice[S ~[]V, V any](slice S, i int) (prev int, value V) { + if i < 0 { + return len(slice) - 1, slice[len(slice)-1] + } + prev = i - 1 + if prev == -1 { + prev = len(slice) - 1 + } + return prev, slice[prev] +} + +// FindCombinationsInSliceByRange 获取给定数组的所有组合,且每个组合的成员数量限制在指定范围内 +func FindCombinationsInSliceByRange[S ~[]V, V any](s S, minSize, maxSize int) []S { + n := len(s) + if n == 0 || minSize <= 0 || maxSize <= 0 || minSize > maxSize { + return nil + } + + var result []S + var currentCombination S + + var backtrack func(startIndex int, currentSize int) + backtrack = func(startIndex int, currentSize int) { + if currentSize >= minSize && currentSize <= maxSize { + combination := make(S, len(currentCombination)) + copy(combination, currentCombination) + result = append(result, combination) + } + + for i := startIndex; i < n; i++ { + currentCombination = append(currentCombination, s[i]) + backtrack(i+1, currentSize+1) + currentCombination = currentCombination[:len(currentCombination)-1] + } + } + + backtrack(0, 0) + return result +} + +// FindFirstOrDefaultInSlice 判断切片中是否存在元素,返回第一个元素,不存在则返回默认值 +func FindFirstOrDefaultInSlice[S ~[]V, V any](slice S, defaultValue V) V { + if len(slice) == 0 { + return defaultValue + } + return slice[0] +} + +// FindOrDefaultInSlice 判断切片中是否存在某个元素,返回第一个匹配的索引和元素,不存在则返回默认值 +func FindOrDefaultInSlice[S ~[]V, V any](slice S, defaultValue V, handler func(v V) bool) (t V) { + if len(slice) == 0 { + return defaultValue + } + for _, v := range slice { + if handler(v) { + return v + } + } + return defaultValue +} + +// FindOrDefaultInComparableSlice 判断切片中是否存在某个元素,返回第一个匹配的索引和元素,不存在则返回默认值 +func FindOrDefaultInComparableSlice[S ~[]V, V comparable](slice S, v V, defaultValue V) (t V) { + if len(slice) == 0 { + return defaultValue + } + for _, value := range slice { + if value == v { + return value + } + } + return defaultValue +} + +// FindInSlice 判断切片中是否存在某个元素,返回第一个匹配的索引和元素,不存在则索引返回 -1 +func FindInSlice[S ~[]V, V any](slice S, handler func(v V) bool) (i int, t V) { + if len(slice) == 0 { + return -1, t + } + for i, v := range slice { + if handler(v) { + return i, v + } + } + return -1, t +} + +// FindIndexInSlice 判断切片中是否存在某个元素,返回第一个匹配的索引,不存在则索引返回 -1 +func FindIndexInSlice[S ~[]V, V any](slice S, handler func(v V) bool) int { + if len(slice) == 0 { + return -1 + } + for i, v := range slice { + if handler(v) { + return i + } + } + return -1 +} + +// FindInComparableSlice 判断切片中是否存在某个元素,返回第一个匹配的索引和元素,不存在则索引返回 -1 +func FindInComparableSlice[S ~[]V, V comparable](slice S, v V) (i int, t V) { + if len(slice) == 0 { + return -1, t + } + for i, value := range slice { + if value == v { + return i, value + } + } + return -1, t +} + +// FindIndexInComparableSlice 判断切片中是否存在某个元素,返回第一个匹配的索引,不存在则索引返回 -1 +func FindIndexInComparableSlice[S ~[]V, V comparable](slice S, v V) int { + if len(slice) == 0 { + return -1 + } + for i, value := range slice { + if value == v { + return i + } + } + return -1 +} + +// FindMinimumInComparableSlice 获取切片中的最小值 +func FindMinimumInComparableSlice[S ~[]V, V generic.Ordered](slice S) (result V) { + if len(slice) == 0 { + return + } + result = slice[0] + for i := 1; i < len(slice); i++ { + if result > slice[i] { + result = slice[i] + } + } + return +} + +// FindMinimumInSlice 获取切片中的最小值 +func FindMinimumInSlice[S ~[]V, V any, N generic.Ordered](slice S, handler OrderedValueGetter[V, N]) (result V) { + if len(slice) == 0 { + return + } + result = slice[0] + for i := 1; i < len(slice); i++ { + if handler(result) > handler(slice[i]) { + result = slice[i] + } + } + return +} + +// FindMaximumInComparableSlice 获取切片中的最大值 +func FindMaximumInComparableSlice[S ~[]V, V generic.Ordered](slice S) (result V) { + if len(slice) == 0 { + return + } + result = slice[0] + for i := 1; i < len(slice); i++ { + if result < slice[i] { + result = slice[i] + } + } + return +} + +// FindMaximumInSlice 获取切片中的最大值 +func FindMaximumInSlice[S ~[]V, V any, N generic.Ordered](slice S, handler OrderedValueGetter[V, N]) (result V) { + if len(slice) == 0 { + return + } + result = slice[0] + for i := 1; i < len(slice); i++ { + if handler(result) < handler(slice[i]) { + result = slice[i] + } + } + return +} + +// FindMin2MaxInComparableSlice 获取切片中的最小值和最大值 +func FindMin2MaxInComparableSlice[S ~[]V, V generic.Ordered](slice S) (min, max V) { + if len(slice) == 0 { + return + } + min = slice[0] + max = slice[0] + for i := 1; i < len(slice); i++ { + if min > slice[i] { + min = slice[i] + } + if max < slice[i] { + max = slice[i] + } + } + return +} + +// FindMin2MaxInSlice 获取切片中的最小值和最大值 +func FindMin2MaxInSlice[S ~[]V, V any, N generic.Ordered](slice S, handler OrderedValueGetter[V, N]) (min, max V) { + if len(slice) == 0 { + return + } + min = slice[0] + max = slice[0] + for i := 1; i < len(slice); i++ { + if handler(min) > handler(slice[i]) { + min = slice[i] + } + if handler(max) < handler(slice[i]) { + max = slice[i] + } + } + return +} + +// FindMinFromComparableMap 获取 map 中的最小值 +func FindMinFromComparableMap[M ~map[K]V, K comparable, V generic.Ordered](m M) (result V) { + if m == nil { + return + } + var first bool + for _, v := range m { + if !first { + result = v + first = true + continue + } + if result > v { + result = v + } + } + return +} + +// FindMinFromMap 获取 map 中的最小值 +func FindMinFromMap[M ~map[K]V, K comparable, V any, N generic.Ordered](m M, handler OrderedValueGetter[V, N]) (result V) { + if m == nil { + return + } + var first bool + for _, v := range m { + if !first { + result = v + first = true + continue + } + if handler(result) > handler(v) { + result = v + } + } + return +} + +// FindMaxFromComparableMap 获取 map 中的最大值 +func FindMaxFromComparableMap[M ~map[K]V, K comparable, V generic.Ordered](m M) (result V) { + if m == nil { + return + } + for _, v := range m { + if result < v { + result = v + } + } + return +} + +// FindMaxFromMap 获取 map 中的最大值 +func FindMaxFromMap[M ~map[K]V, K comparable, V any, N generic.Ordered](m M, handler OrderedValueGetter[V, N]) (result V) { + if m == nil { + return + } + for _, v := range m { + if handler(result) < handler(v) { + result = v + } + } + return +} + +// FindMin2MaxFromComparableMap 获取 map 中的最小值和最大值 +func FindMin2MaxFromComparableMap[M ~map[K]V, K comparable, V generic.Ordered](m M) (min, max V) { + if m == nil { + return + } + var first bool + for _, v := range m { + if !first { + min = v + max = v + first = true + continue + } + if min > v { + min = v + } + if max < v { + max = v + } + } + return +} + +// FindMin2MaxFromMap 获取 map 中的最小值和最大值 +func FindMin2MaxFromMap[M ~map[K]V, K comparable, V generic.Ordered](m M) (min, max V) { + if m == nil { + return + } + var first bool + for _, v := range m { + if !first { + min = v + max = v + first = true + continue + } + if min > v { + min = v + } + if max < v { + max = v + } + } + return +} diff --git a/toolkit/collection/find_example_test.go b/toolkit/collection/find_example_test.go new file mode 100644 index 0000000..3c73c5e --- /dev/null +++ b/toolkit/collection/find_example_test.go @@ -0,0 +1,176 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleFindLoopedNextInSlice() { + next, v := collection.FindLoopedNextInSlice([]int{1, 2, 3}, 1) + fmt.Println(next, v) + // Output: + // 2 3 +} + +func ExampleFindLoopedPrevInSlice() { + prev, v := collection.FindLoopedPrevInSlice([]int{1, 2, 3}, 1) + fmt.Println(prev, v) + // Output: + // 0 1 +} + +func ExampleFindCombinationsInSliceByRange() { + result := collection.FindCombinationsInSliceByRange([]int{1, 2, 3}, 1, 2) + fmt.Println(len(result)) + // Output: + // 6 +} + +func ExampleFindFirstOrDefaultInSlice() { + result := collection.FindFirstOrDefaultInSlice([]int{1, 2, 3}, 0) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindOrDefaultInSlice() { + result := collection.FindOrDefaultInSlice([]int{1, 2, 3}, 0, func(v int) bool { + return v == 2 + }) + fmt.Println(result) + // Output: + // 2 +} + +func ExampleFindOrDefaultInComparableSlice() { + result := collection.FindOrDefaultInComparableSlice([]int{1, 2, 3}, 2, 0) + fmt.Println(result) + // Output: + // 2 +} + +func ExampleFindInSlice() { + _, result := collection.FindInSlice([]int{1, 2, 3}, func(v int) bool { + return v == 2 + }) + fmt.Println(result) + // Output: + // 2 +} + +func ExampleFindIndexInSlice() { + result := collection.FindIndexInSlice([]int{1, 2, 3}, func(v int) bool { + return v == 2 + }) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindInComparableSlice() { + index, result := collection.FindInComparableSlice([]int{1, 2, 3}, 2) + fmt.Println(index, result) + // Output: + // 1 2 +} + +func ExampleFindIndexInComparableSlice() { + result := collection.FindIndexInComparableSlice([]int{1, 2, 3}, 2) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindMinimumInComparableSlice() { + result := collection.FindMinimumInComparableSlice([]int{1, 2, 3}) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindMinimumInSlice() { + result := collection.FindMinimumInSlice([]int{1, 2, 3}, func(v int) int { + return v + }) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindMaximumInComparableSlice() { + result := collection.FindMaximumInComparableSlice([]int{1, 2, 3}) + fmt.Println(result) + // Output: + // 3 +} + +func ExampleFindMaximumInSlice() { + result := collection.FindMaximumInSlice([]int{1, 2, 3}, func(v int) int { + return v + }) + fmt.Println(result) + // Output: + // 3 +} + +func ExampleFindMin2MaxInComparableSlice() { + minimum, maximum := collection.FindMin2MaxInComparableSlice([]int{1, 2, 3}) + fmt.Println(minimum, maximum) + // Output: + // 1 3 +} + +func ExampleFindMin2MaxInSlice() { + minimum, maximum := collection.FindMin2MaxInSlice([]int{1, 2, 3}, func(v int) int { + return v + }) + fmt.Println(minimum, maximum) + // Output: + // 1 3 +} + +func ExampleFindMinFromComparableMap() { + result := collection.FindMinFromComparableMap(map[int]int{1: 1, 2: 2, 3: 3}) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindMinFromMap() { + result := collection.FindMinFromMap(map[int]int{1: 1, 2: 2, 3: 3}, func(v int) int { + return v + }) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleFindMaxFromComparableMap() { + result := collection.FindMaxFromComparableMap(map[int]int{1: 1, 2: 2, 3: 3}) + fmt.Println(result) + // Output: + // 3 +} + +func ExampleFindMaxFromMap() { + result := collection.FindMaxFromMap(map[int]int{1: 1, 2: 2, 3: 3}, func(v int) int { + return v + }) + fmt.Println(result) + // Output: + // 3 +} + +func ExampleFindMin2MaxFromComparableMap() { + minimum, maximum := collection.FindMin2MaxFromComparableMap(map[int]int{1: 1, 2: 2, 3: 3}) + fmt.Println(minimum, maximum) + // Output: + // 1 3 +} + +func ExampleFindMin2MaxFromMap() { + minimum, maximum := collection.FindMin2MaxFromMap(map[int]int{1: 1, 2: 2, 3: 3}) + fmt.Println(minimum, maximum) + // Output: + // 1 3 +} diff --git a/toolkit/collection/find_test.go b/toolkit/collection/find_test.go new file mode 100644 index 0000000..8838ea6 --- /dev/null +++ b/toolkit/collection/find_test.go @@ -0,0 +1,467 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestFindLoopedNextInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + i int + expected int + }{ + {"TestFindLoopedNextInSlice_NonEmptySlice", []int{1, 2, 3}, 1, 2}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, _ := collection.FindLoopedNextInSlice(c.input, c.i) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the next index of input is not equal") + } + }) + } +} + +func TestFindLoopedPrevInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + i int + expected int + }{ + {"TestFindLoopedPrevInSlice_NonEmptySlice", []int{1, 2, 3}, 1, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual, _ := collection.FindLoopedPrevInSlice(c.input, c.i) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the prev index of input is not equal") + } + }) + } +} + +func TestFindCombinationsInSliceByRange(t *testing.T) { + var cases = []struct { + name string + input []int + minSize int + maxSize int + expected [][]int + }{ + {"TestFindCombinationsInSliceByRange_NonEmptySlice", []int{1, 2, 3}, 1, 2, [][]int{{1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindCombinationsInSliceByRange(c.input, c.minSize, c.maxSize) + if len(actual) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the length of input is not equal") + } + }) + } +} + +func TestFindFirstOrDefaultInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindFirstOrDefaultInSlice_NonEmptySlice", []int{1, 2, 3}, 1}, + {"TestFindFirstOrDefaultInSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindFirstOrDefaultInSlice(c.input, 0) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the first element of input is not equal") + } + }) + } +} + +func TestFindOrDefaultInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindOrDefaultInSlice_NonEmptySlice", []int{1, 2, 3}, 2}, + {"TestFindOrDefaultInSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindOrDefaultInSlice(c.input, 0, func(v int) bool { + return v == 2 + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the element of input is not equal") + } + }) + } +} + +func TestFindOrDefaultInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindOrDefaultInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 2}, + {"TestFindOrDefaultInComparableSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindOrDefaultInComparableSlice(c.input, 2, 0) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the element of input is not equal") + } + }) + } +} + +func TestFindInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindInSlice_NonEmptySlice", []int{1, 2, 3}, 2}, + {"TestFindInSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, actual := collection.FindInSlice(c.input, func(v int) bool { + return v == 2 + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the element of input is not equal") + } + }) + } +} + +func TestFindIndexInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindIndexInSlice_NonEmptySlice", []int{1, 2, 3}, 1}, + {"TestFindIndexInSlice_EmptySlice", []int{}, -1}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindIndexInSlice(c.input, func(v int) bool { + return v == 2 + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the index of input is not equal") + } + }) + } +} + +func TestFindInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 2}, + {"TestFindInComparableSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, actual := collection.FindInComparableSlice(c.input, 2) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the element of input is not equal") + } + }) + } +} + +func TestFindIndexInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindIndexInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 1}, + {"TestFindIndexInComparableSlice_EmptySlice", []int{}, -1}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindIndexInComparableSlice(c.input, 2) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the index of input is not equal") + } + }) + } +} + +func TestFindMinimumInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindMinimumInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 1}, + {"TestFindMinimumInComparableSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMinimumInComparableSlice(c.input) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMinimumInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindMinimumInSlice_NonEmptySlice", []int{1, 2, 3}, 1}, + {"TestFindMinimumInSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMinimumInSlice(c.input, func(v int) int { + return v + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMaximumInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindMaximumInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestFindMaximumInComparableSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMaximumInComparableSlice(c.input) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the maximum of input is not equal") + } + }) + } +} + +func TestFindMaximumInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestFindMaximumInSlice_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestFindMaximumInSlice_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMaximumInSlice(c.input, func(v int) int { + return v + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the maximum of input is not equal") + } + }) + } +} + +func TestFindMin2MaxInComparableSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expectedMin int + expectedMax int + }{ + {"TestFindMin2MaxInComparableSlice_NonEmptySlice", []int{1, 2, 3}, 1, 3}, + {"TestFindMin2MaxInComparableSlice_EmptySlice", []int{}, 0, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + minimum, maximum := collection.FindMin2MaxInComparableSlice(c.input) + if minimum != c.expectedMin || maximum != c.expectedMax { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expectedMin, minimum, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMin2MaxInSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expectedMin int + expectedMax int + }{ + {"TestFindMin2MaxInSlice_NonEmptySlice", []int{1, 2, 3}, 1, 3}, + {"TestFindMin2MaxInSlice_EmptySlice", []int{}, 0, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + minimum, maximum := collection.FindMin2MaxInSlice(c.input, func(v int) int { + return v + }) + if minimum != c.expectedMin || maximum != c.expectedMax { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expectedMin, minimum, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMinFromComparableMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected int + }{ + {"TestFindMinFromComparableMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1}, + {"TestFindMinFromComparableMap_EmptyMap", map[int]int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMinFromComparableMap(c.input) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMinFromMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected int + }{ + {"TestFindMinFromMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1}, + {"TestFindMinFromMap_EmptyMap", map[int]int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMinFromMap(c.input, func(v int) int { + return v + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMaxFromComparableMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected int + }{ + {"TestFindMaxFromComparableMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 3}, + {"TestFindMaxFromComparableMap_EmptyMap", map[int]int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMaxFromComparableMap(c.input) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the maximum of input is not equal") + } + }) + } +} + +func TestFindMaxFromMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected int + }{ + {"TestFindMaxFromMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 3}, + {"TestFindMaxFromMap_EmptyMap", map[int]int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.FindMaxFromMap(c.input, func(v int) int { + return v + }) + if actual != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, actual, "the maximum of input is not equal") + } + }) + } +} + +func TestFindMin2MaxFromComparableMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expectedMin int + expectedMax int + }{ + {"TestFindMin2MaxFromComparableMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1, 3}, + {"TestFindMin2MaxFromComparableMap_EmptyMap", map[int]int{}, 0, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + minimum, maximum := collection.FindMin2MaxFromComparableMap(c.input) + if minimum != c.expectedMin || maximum != c.expectedMax { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expectedMin, minimum, "the minimum of input is not equal") + } + }) + } +} + +func TestFindMin2MaxFromMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expectedMin int + expectedMax int + }{ + {"TestFindMin2MaxFromMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, 1, 3}, + {"TestFindMin2MaxFromMap_EmptyMap", map[int]int{}, 0, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + minimum, maximum := collection.FindMin2MaxFromMap(c.input) + if minimum != c.expectedMin || maximum != c.expectedMax { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expectedMin, minimum, "the minimum of input is not equal") + } + }) + } +} diff --git a/toolkit/collection/item.go b/toolkit/collection/item.go new file mode 100644 index 0000000..fc9fd40 --- /dev/null +++ b/toolkit/collection/item.go @@ -0,0 +1,9 @@ +package collection + +// SwapSlice 将切片中的两个元素进行交换 +func SwapSlice[S ~[]V, V any](slice *S, i, j int) { + if i < 0 || j < 0 || i >= len(*slice) || j >= len(*slice) { + return + } + (*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i] +} diff --git a/toolkit/collection/item_example_test.go b/toolkit/collection/item_example_test.go new file mode 100644 index 0000000..277ee7a --- /dev/null +++ b/toolkit/collection/item_example_test.go @@ -0,0 +1,14 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleSwapSlice() { + var s = []int{1, 2, 3} + collection.SwapSlice(&s, 0, 1) + fmt.Println(s) + // Output: + // [2 1 3] +} diff --git a/toolkit/collection/item_test.go b/toolkit/collection/item_test.go new file mode 100644 index 0000000..faf36a3 --- /dev/null +++ b/toolkit/collection/item_test.go @@ -0,0 +1,32 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestSwapSlice(t *testing.T) { + var cases = []struct { + name string + slice []int + i int + j int + expect []int + }{ + {"TestSwapSliceNonEmpty", []int{1, 2, 3}, 0, 1, []int{2, 1, 3}}, + {"TestSwapSliceEmpty", []int{}, 0, 0, []int{}}, + {"TestSwapSliceIndexOutOfBound", []int{1, 2, 3}, 0, 3, []int{1, 2, 3}}, + {"TestSwapSliceNil", nil, 0, 0, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.SwapSlice(&c.slice, c.i, c.j) + for i, v := range c.slice { + if v != c.expect[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expect, c.slice, "the slice is not equal") + } + } + }) + } +} diff --git a/toolkit/collection/listings/paged_slice.go b/toolkit/collection/listings/paged_slice.go new file mode 100644 index 0000000..d58c29d --- /dev/null +++ b/toolkit/collection/listings/paged_slice.go @@ -0,0 +1,68 @@ +package listings + +// NewPagedSlice 创建一个新的 PagedSlice 实例。 +func NewPagedSlice[T any](pageSize int) *PagedSlice[T] { + return &PagedSlice[T]{ + pages: make([][]T, 0, pageSize), + pageSize: pageSize, + } +} + +// PagedSlice 是一个高效的动态数组,它通过分页管理内存并减少频繁的内存分配来提高性能。 +type PagedSlice[T any] struct { + pages [][]T + pageSize int + len int + lenLast int +} + +// Add 添加一个元素到 PagedSlice 中。 +func (slf *PagedSlice[T]) Add(value T) { + if slf.lenLast == len(slf.pages[len(slf.pages)-1]) { + slf.pages = append(slf.pages, make([]T, slf.pageSize)) + slf.lenLast = 0 + } + + slf.pages[len(slf.pages)-1][slf.lenLast] = value + slf.len++ + slf.lenLast++ +} + +// Get 获取 PagedSlice 中给定索引的元素。 +func (slf *PagedSlice[T]) Get(index int) *T { + if index < 0 || index >= slf.len { + return nil + } + + return &slf.pages[index/slf.pageSize][index%slf.pageSize] +} + +// Set 设置 PagedSlice 中给定索引的元素。 +func (slf *PagedSlice[T]) Set(index int, value T) { + if index < 0 || index >= slf.len { + return + } + + slf.pages[index/slf.pageSize][index%slf.pageSize] = value +} + +// Len 返回 PagedSlice 中元素的数量。 +func (slf *PagedSlice[T]) Len() int { + return slf.len +} + +// Clear 清空 PagedSlice。 +func (slf *PagedSlice[T]) Clear() { + slf.pages = make([][]T, 0) + slf.len = 0 + slf.lenLast = 0 +} + +// Range 迭代 PagedSlice 中的所有元素。 +func (slf *PagedSlice[T]) Range(f func(index int, value T) bool) { + for i := 0; i < slf.len; i++ { + if !f(i, slf.pages[i/slf.pageSize][i%slf.pageSize]) { + return + } + } +} diff --git a/toolkit/collection/listings/priority_slice.go b/toolkit/collection/listings/priority_slice.go new file mode 100644 index 0000000..fe70d99 --- /dev/null +++ b/toolkit/collection/listings/priority_slice.go @@ -0,0 +1,207 @@ +package listings + +import ( + "fmt" + "sort" +) + +// NewPrioritySlice 创建一个优先级切片,优先级越低越靠前 +func NewPrioritySlice[V any](lengthAndCap ...int) *PrioritySlice[V] { + p := &PrioritySlice[V]{} + if len(lengthAndCap) > 0 { + var length = lengthAndCap[0] + var c int + if len(lengthAndCap) > 1 { + c = lengthAndCap[1] + } + p.items = make([]*priorityItem[V], length, c) + } + return p +} + +// PrioritySlice 是一个优先级切片,优先级越低越靠前 +type PrioritySlice[V any] struct { + items []*priorityItem[V] +} + +// Len 返回切片长度 +func (slf *PrioritySlice[V]) Len() int { + return len(slf.items) +} + +// Cap 返回切片容量 +func (slf *PrioritySlice[V]) Cap() int { + return cap(slf.items) +} + +// Clear 清空切片 +func (slf *PrioritySlice[V]) Clear() { + slf.items = slf.items[:0] +} + +// Append 添加元素 +func (slf *PrioritySlice[V]) Append(v V, p int) { + slf.items = append(slf.items, &priorityItem[V]{ + v: v, + p: p, + }) + slf.sort() +} + +// Appends 添加元素 +func (slf *PrioritySlice[V]) Appends(priority int, vs ...V) { + for _, v := range vs { + slf.Append(v, priority) + } + slf.sort() +} + +// AppendByOptionalPriority 添加元素 +func (slf *PrioritySlice[V]) AppendByOptionalPriority(v V, priority ...int) { + if len(priority) == 0 { + slf.Append(v, 0) + } else { + slf.Append(v, priority[0]) + } +} + +// Get 获取元素 +func (slf *PrioritySlice[V]) Get(index int) (V, int) { + i := slf.items[index] + return i.Value(), i.Priority() +} + +// GetValue 获取元素值 +func (slf *PrioritySlice[V]) GetValue(index int) V { + return slf.items[index].Value() +} + +// GetPriority 获取元素优先级 +func (slf *PrioritySlice[V]) GetPriority(index int) int { + return slf.items[index].Priority() +} + +// Set 设置元素 +func (slf *PrioritySlice[V]) Set(index int, value V, priority int) { + before := slf.items[index] + slf.items[index] = &priorityItem[V]{ + v: value, + p: priority, + } + if before.Priority() != priority { + slf.sort() + } +} + +// SetValue 设置元素值 +func (slf *PrioritySlice[V]) SetValue(index int, value V) { + slf.items[index].v = value +} + +// SetPriority 设置元素优先级 +func (slf *PrioritySlice[V]) SetPriority(index int, priority int) { + slf.items[index].p = priority + slf.sort() +} + +// Action 直接操作切片,如果返回值不为 nil,则替换切片 +func (slf *PrioritySlice[V]) Action(action func(items []*priorityItem[V]) []*priorityItem[V]) { + if len(slf.items) == 0 { + return + } + if replace := action(slf.items); replace != nil { + slf.items = replace + slf.sort() + } +} + +// Range 遍历切片,如果返回值为 false,则停止遍历 +func (slf *PrioritySlice[V]) Range(action func(index int, item *priorityItem[V]) bool) { + for i, item := range slf.items { + if !action(i, item) { + break + } + } +} + +// RangeValue 遍历切片值,如果返回值为 false,则停止遍历 +func (slf *PrioritySlice[V]) RangeValue(action func(index int, value V) bool) { + slf.Range(func(index int, item *priorityItem[V]) bool { + return action(index, item.Value()) + }) +} + +// RangePriority 遍历切片优先级,如果返回值为 false,则停止遍历 +func (slf *PrioritySlice[V]) RangePriority(action func(index int, priority int) bool) { + slf.Range(func(index int, item *priorityItem[V]) bool { + return action(index, item.Priority()) + }) +} + +// SyncSlice 返回切片 +func (slf *PrioritySlice[V]) Slice() []V { + var vs []V + for _, item := range slf.items { + vs = append(vs, item.Value()) + } + return vs +} + +// String 返回切片字符串 +func (slf *PrioritySlice[V]) String() string { + var vs []V + for _, item := range slf.items { + vs = append(vs, item.Value()) + } + return fmt.Sprint(vs) +} + +// sort 排序 +func (slf *PrioritySlice[V]) sort() { + if len(slf.items) <= 1 { + return + } + sort.Slice(slf.items, func(i, j int) bool { + return slf.items[i].Priority() < slf.items[j].Priority() + }) + for i := 0; i < len(slf.items); i++ { + if i == 0 { + slf.items[i].prev = nil + slf.items[i].next = slf.items[i+1] + } else if i == len(slf.items)-1 { + slf.items[i].prev = slf.items[i-1] + slf.items[i].next = nil + } else { + slf.items[i].prev = slf.items[i-1] + slf.items[i].next = slf.items[i+1] + } + } +} + +// priorityItem 是一个优先级切片元素 +type priorityItem[V any] struct { + next *priorityItem[V] + prev *priorityItem[V] + v V + p int +} + +// Value 返回元素值 +func (p *priorityItem[V]) Value() V { + return p.v +} + +// Priority 返回元素优先级 +func (p *priorityItem[V]) Priority() int { + return p.p +} + +// Next 返回下一个元素 +func (p *priorityItem[V]) Next() *priorityItem[V] { + return p.next +} + +// Prev 返回上一个元素 +func (p *priorityItem[V]) Prev() *priorityItem[V] { + return p.prev +} diff --git a/toolkit/collection/listings/priority_slice_test.go b/toolkit/collection/listings/priority_slice_test.go new file mode 100644 index 0000000..c8d3ac8 --- /dev/null +++ b/toolkit/collection/listings/priority_slice_test.go @@ -0,0 +1,14 @@ +package listings_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection/listings" + "testing" +) + +func TestPrioritySlice_Append(t *testing.T) { + var s = listings.NewPrioritySlice[string]() + s.Append("name_1", 2) + s.Append("name_2", 1) + fmt.Println(s) +} diff --git a/toolkit/collection/listings/sync_priority_slice.go b/toolkit/collection/listings/sync_priority_slice.go new file mode 100644 index 0000000..b1b5083 --- /dev/null +++ b/toolkit/collection/listings/sync_priority_slice.go @@ -0,0 +1,209 @@ +package listings + +import ( + "fmt" + "sort" + "sync" +) + +// NewSyncPrioritySlice 创建一个并发安全的优先级切片,优先级越低越靠前 +func NewSyncPrioritySlice[V any](lengthAndCap ...int) *SyncPrioritySlice[V] { + p := &SyncPrioritySlice[V]{} + if len(lengthAndCap) > 0 { + var length = lengthAndCap[0] + var c int + if len(lengthAndCap) > 1 { + c = lengthAndCap[1] + } + p.items = make([]*priorityItem[V], length, c) + } + return p +} + +// SyncPrioritySlice 是一个优先级切片,优先级越低越靠前 +type SyncPrioritySlice[V any] struct { + rw sync.RWMutex + items []*priorityItem[V] +} + +// Len 返回切片长度 +func (slf *SyncPrioritySlice[V]) Len() int { + slf.rw.RLock() + defer slf.rw.RUnlock() + return len(slf.items) +} + +// Cap 返回切片容量 +func (slf *SyncPrioritySlice[V]) Cap() int { + slf.rw.RLock() + defer slf.rw.RUnlock() + return cap(slf.items) +} + +// Clear 清空切片 +func (slf *SyncPrioritySlice[V]) Clear() { + slf.rw.Lock() + defer slf.rw.Unlock() + slf.items = slf.items[:0] +} + +// Append 添加元素 +func (slf *SyncPrioritySlice[V]) Append(v V, p int) { + slf.rw.Lock() + defer slf.rw.Unlock() + slf.items = append(slf.items, &priorityItem[V]{ + v: v, + p: p, + }) + slf.sort() +} + +// Appends 添加元素 +func (slf *SyncPrioritySlice[V]) Appends(priority int, vs ...V) { + for _, v := range vs { + slf.Append(v, priority) + } + slf.sort() +} + +// AppendByOptionalPriority 添加元素 +func (slf *SyncPrioritySlice[V]) AppendByOptionalPriority(v V, priority ...int) { + if len(priority) == 0 { + slf.Append(v, 0) + } else { + slf.Append(v, priority[0]) + } +} + +// Get 获取元素 +func (slf *SyncPrioritySlice[V]) Get(index int) (V, int) { + slf.rw.RLock() + defer slf.rw.RUnlock() + i := slf.items[index] + return i.Value(), i.Priority() +} + +// GetValue 获取元素值 +func (slf *SyncPrioritySlice[V]) GetValue(index int) V { + slf.rw.RLock() + defer slf.rw.RUnlock() + return slf.items[index].Value() +} + +// GetPriority 获取元素优先级 +func (slf *SyncPrioritySlice[V]) GetPriority(index int) int { + slf.rw.RLock() + defer slf.rw.RUnlock() + return slf.items[index].Priority() +} + +// Set 设置元素 +func (slf *SyncPrioritySlice[V]) Set(index int, value V, priority int) { + slf.rw.Lock() + defer slf.rw.Unlock() + before := slf.items[index] + slf.items[index] = &priorityItem[V]{ + v: value, + p: priority, + } + if before.Priority() != priority { + slf.sort() + } +} + +// SetValue 设置元素值 +func (slf *SyncPrioritySlice[V]) SetValue(index int, value V) { + slf.rw.Lock() + defer slf.rw.Unlock() + slf.items[index].v = value +} + +// SetPriority 设置元素优先级 +func (slf *SyncPrioritySlice[V]) SetPriority(index int, priority int) { + slf.rw.Lock() + defer slf.rw.Unlock() + slf.items[index].p = priority + slf.sort() +} + +// Action 直接操作切片,如果返回值不为 nil,则替换切片 +func (slf *SyncPrioritySlice[V]) Action(action func(items []*priorityItem[V]) []*priorityItem[V]) { + slf.rw.Lock() + defer slf.rw.Unlock() + if len(slf.items) == 0 { + return + } + if replace := action(slf.items); replace != nil { + slf.items = replace + slf.sort() + } +} + +// Range 遍历切片,如果返回值为 false,则停止遍历 +func (slf *SyncPrioritySlice[V]) Range(action func(index int, item *priorityItem[V]) bool) { + slf.rw.RLock() + defer slf.rw.RUnlock() + for i, item := range slf.items { + if !action(i, item) { + break + } + } +} + +// RangeValue 遍历切片值,如果返回值为 false,则停止遍历 +func (slf *SyncPrioritySlice[V]) RangeValue(action func(index int, value V) bool) { + slf.Range(func(index int, item *priorityItem[V]) bool { + return action(index, item.Value()) + }) +} + +// RangePriority 遍历切片优先级,如果返回值为 false,则停止遍历 +func (slf *SyncPrioritySlice[V]) RangePriority(action func(index int, priority int) bool) { + slf.Range(func(index int, item *priorityItem[V]) bool { + return action(index, item.Priority()) + }) +} + +// Slice 返回切片 +func (slf *SyncPrioritySlice[V]) Slice() []V { + slf.rw.RLock() + defer slf.rw.RUnlock() + var vs []V + for _, item := range slf.items { + vs = append(vs, item.Value()) + } + return vs +} + +// String 返回切片字符串 +func (slf *SyncPrioritySlice[V]) String() string { + slf.rw.RLock() + defer slf.rw.RUnlock() + var vs []V + for _, item := range slf.items { + vs = append(vs, item.Value()) + } + return fmt.Sprint(vs) +} + +// sort 排序 +func (slf *SyncPrioritySlice[V]) sort() { + if len(slf.items) <= 1 { + return + } + sort.Slice(slf.items, func(i, j int) bool { + return slf.items[i].Priority() < slf.items[j].Priority() + }) + for i := 0; i < len(slf.items); i++ { + if i == 0 { + slf.items[i].prev = nil + slf.items[i].next = slf.items[i+1] + } else if i == len(slf.items)-1 { + slf.items[i].prev = slf.items[i-1] + slf.items[i].next = nil + } else { + slf.items[i].prev = slf.items[i-1] + slf.items[i].next = slf.items[i+1] + } + } +} diff --git a/toolkit/collection/listings/sync_slice.go b/toolkit/collection/listings/sync_slice.go new file mode 100644 index 0000000..3299289 --- /dev/null +++ b/toolkit/collection/listings/sync_slice.go @@ -0,0 +1,61 @@ +package listings + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "sync" +) + +// NewSyncSlice 创建一个 SyncSlice +func NewSyncSlice[V any](length, cap int) *SyncSlice[V] { + s := &SyncSlice[V]{} + if length > 0 || cap > 0 { + s.data = make([]V, length, cap) + } + return s +} + +// SyncSlice 是基于 sync.RWMutex 实现的线程安全的 slice +type SyncSlice[V any] struct { + rw sync.RWMutex + data []V +} + +func (slf *SyncSlice[V]) Get(index int) V { + slf.rw.RLock() + defer slf.rw.RUnlock() + return slf.data[index] +} + +func (slf *SyncSlice[V]) GetWithRange(start, end int) []V { + return slf.data[start:end] +} + +func (slf *SyncSlice[V]) Set(index int, value V) { + slf.rw.Lock() + slf.data[index] = value + slf.rw.Unlock() +} + +func (slf *SyncSlice[V]) Append(values ...V) { + slf.rw.Lock() + slf.data = append(slf.data, values...) + slf.rw.Unlock() +} + +func (slf *SyncSlice[V]) Release() { + slf.rw.Lock() + slf.data = nil + slf.rw.Unlock() +} + +func (slf *SyncSlice[V]) Clear() { + slf.rw.Lock() + slf.data = slf.data[:0] + slf.rw.Unlock() +} + +func (slf *SyncSlice[V]) GetData() []V { + slf.rw.Lock() + defer slf.rw.Unlock() + return collection.CloneSlice(slf.data) +} diff --git a/toolkit/collection/loop.go b/toolkit/collection/loop.go new file mode 100644 index 0000000..9b3700c --- /dev/null +++ b/toolkit/collection/loop.go @@ -0,0 +1,188 @@ +package collection + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "sort" +) + +// LoopSlice 迭代切片 slice 中的每一个函数,并将索引和值传递给 f 函数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopSlice[S ~[]V, V any](slice S, f func(i int, val V) bool) { + for i, v := range slice { + if !f(i, v) { + break + } + } +} + +// ReverseLoopSlice 逆序迭代切片 slice 中的每一个函数,并将索引和值传递给 f 函数 +// - 迭代过程将在 f 函数返回 false 时中断 +func ReverseLoopSlice[S ~[]V, V any](slice S, f func(i int, val V) bool) { + for i := len(slice) - 1; i >= 0; i-- { + if !f(i, slice[i]) { + break + } + } +} + +// LoopMap 迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - m 的迭代顺序是不确定的,因此每次迭代的顺序可能不同 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMap[M ~map[K]V, K comparable, V any](m M, f func(i int, key K, val V) bool) { + var i int + for k, v := range m { + if !f(i, k, v) { + break + } + i++ + } +} + +// LoopMapByOrderedKeyAsc 按照键的升序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByOrderedKeyAsc[M ~map[K]V, K generic.Ordered, V any](m M, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return AscBy(keys[i], keys[j]) + }) + for i, k := range keys { + if !f(i, k, m[k]) { + break + } + } +} + +// LoopMapByOrderedKeyDesc 按照键的降序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByOrderedKeyDesc[M ~map[K]V, K generic.Ordered, V any](m M, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return DescBy(keys[i], keys[j]) + }) + for i, k := range keys { + if !f(i, k, m[k]) { + break + } + } +} + +// LoopMapByOrderedValueAsc 按照值的升序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByOrderedValueAsc[M ~map[K]V, K comparable, V generic.Ordered](m M, f func(i int, key K, val V) bool) { + var keys []K + var values []V + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + sort.Slice(values, func(i, j int) bool { + return AscBy(values[i], values[j]) + }) + for i, v := range values { + if !f(i, keys[i], v) { + break + } + } +} + +// LoopMapByOrderedValueDesc 按照值的降序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByOrderedValueDesc[M ~map[K]V, K comparable, V generic.Ordered](m M, f func(i int, key K, val V) bool) { + var keys []K + var values []V + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + sort.Slice(values, func(i, j int) bool { + return DescBy(values[i], values[j]) + }) + for i, v := range values { + if !f(i, keys[i], v) { + break + } + } +} + +// LoopMapByKeyGetterAsc 按照键的升序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByKeyGetterAsc[M ~map[K]V, K comparable, V comparable, N generic.Ordered](m M, getter func(k K) N, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return AscBy(getter(keys[i]), getter(keys[j])) + }) + for i, v := range keys { + if !f(i, keys[i], m[v]) { + break + } + } +} + +// LoopMapByValueGetterAsc 按照值的升序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByValueGetterAsc[M ~map[K]V, K comparable, V any, N generic.Ordered](m M, getter func(v V) N, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return AscBy(getter(m[keys[i]]), getter(m[keys[j]])) + }) + for i, v := range keys { + if !f(i, keys[i], m[v]) { + break + } + } +} + +// LoopMapByKeyGetterDesc 按照键的降序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByKeyGetterDesc[M ~map[K]V, K comparable, V comparable, N generic.Ordered](m M, getter func(k K) N, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return DescBy(getter(keys[i]), getter(keys[j])) + }) + for i, v := range keys { + if !f(i, keys[i], m[v]) { + break + } + } +} + +// LoopMapByValueGetterDesc 按照值的降序迭代 m 中的每一个函数,并将键和值传递给 f 函数 +// - 该函数会在 f 中传入一个从 0 开始的索引,用于表示当前迭代的次数 +// - 迭代过程将在 f 函数返回 false 时中断 +func LoopMapByValueGetterDesc[M ~map[K]V, K comparable, V any, N generic.Ordered](m M, getter func(v V) N, f func(i int, key K, val V) bool) { + var keys []K + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return DescBy(getter(m[keys[i]]), getter(m[keys[j]])) + }) + for i, v := range keys { + if !f(i, keys[i], m[v]) { + break + } + } +} diff --git a/toolkit/collection/loop_example_test.go b/toolkit/collection/loop_example_test.go new file mode 100644 index 0000000..f725606 --- /dev/null +++ b/toolkit/collection/loop_example_test.go @@ -0,0 +1,159 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleLoopSlice() { + var result []int + collection.LoopSlice([]int{1, 2, 3, 4, 5}, func(i int, val int) bool { + result = append(result, val) + if uint(i) == 1 { + return false + } + return true + }) + fmt.Println(result) + // Output: [1 2] +} + +func ExampleReverseLoopSlice() { + var result []int + collection.ReverseLoopSlice([]int{1, 2, 3, 4, 5}, func(i int, val int) bool { + result = append(result, val) + if uint(i) == 1 { + return false + } + return true + }) + fmt.Println(result) + // Output: [5 4 3 2] +} + +func ExampleLoopMap() { + var result []int + collection.LoopMap(map[string]int{"a": 1, "b": 2, "c": 3}, func(i int, key string, val int) bool { + result = append(result, val) + return true + }) + fmt.Println(collection.AllInComparableSlice(result, []int{1, 2, 3})) + // Output: + // true +} + +func ExampleLoopMapByOrderedKeyAsc() { + var result []int + collection.LoopMapByOrderedKeyAsc(map[string]int{"a": 1, "b": 2, "c": 3}, func(i int, key string, val int) bool { + result = append(result, val) + return true + }) + fmt.Println(collection.AllInComparableSlice(result, []int{1, 2, 3})) + // Output: + // true +} + +func ExampleLoopMapByOrderedKeyDesc() { + var result []int + collection.LoopMapByOrderedKeyDesc(map[string]int{"a": 1, "b": 2, "c": 3}, func(i int, key string, val int) bool { + result = append(result, val) + return true + }) + fmt.Println(collection.AllInComparableSlice(result, []int{3, 2, 1})) + // Output: + // true +} + +func ExampleLoopMapByOrderedValueAsc() { + var result []int + collection.LoopMapByOrderedValueAsc(map[string]int{"a": 1, "b": 2, "c": 3}, func(i int, key string, val int) bool { + result = append(result, val) + return true + }) + fmt.Println(collection.AllInComparableSlice(result, []int{1, 2, 3})) + // Output: + // true +} + +func ExampleLoopMapByOrderedValueDesc() { + var result []int + collection.LoopMapByOrderedValueDesc(map[string]int{"a": 1, "b": 2, "c": 3}, func(i int, key string, val int) bool { + result = append(result, val) + return true + }) + fmt.Println(collection.AllInComparableSlice(result, []int{3, 2, 1})) + // Output: + // true +} + +func ExampleLoopMapByKeyGetterAsc() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result []int + collection.LoopMapByKeyGetterAsc( + m, + func(k string) int { + return m[k] + }, + func(i int, key string, val int) bool { + result = append(result, val) + return true + }, + ) + fmt.Println(collection.AllInComparableSlice(result, []int{1, 2, 3})) + // Output: + // true +} + +func ExampleLoopMapByKeyGetterDesc() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result []int + collection.LoopMapByKeyGetterDesc( + m, + func(k string) int { + return m[k] + }, + func(i int, key string, val int) bool { + result = append(result, val) + return true + }, + ) + fmt.Println(collection.AllInComparableSlice(result, []int{3, 2, 1})) + // Output: + // true +} + +func ExampleLoopMapByValueGetterAsc() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result []int + collection.LoopMapByValueGetterAsc( + m, + func(v int) int { + return v + }, + func(i int, key string, val int) bool { + result = append(result, val) + return true + }, + ) + fmt.Println(collection.AllInComparableSlice(result, []int{1, 2, 3})) + // Output: + // true +} + +func ExampleLoopMapByValueGetterDesc() { + var m = map[string]int{"a": 1, "b": 2, "c": 3} + var result []int + collection.LoopMapByValueGetterDesc( + m, + func(v int) int { + return v + }, + func(i int, key string, val int) bool { + result = append(result, val) + return true + }, + ) + fmt.Println(collection.AllInComparableSlice(result, []int{3, 2, 1})) + // Output: + // true +} diff --git a/toolkit/collection/loop_test.go b/toolkit/collection/loop_test.go new file mode 100644 index 0000000..8c0c739 --- /dev/null +++ b/toolkit/collection/loop_test.go @@ -0,0 +1,333 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestLoopSlice(t *testing.T) { + var cases = []struct { + name string + in []int + out []int + breakIndex uint + }{ + {"TestLoopSlice_Part", []int{1, 2, 3, 4, 5}, []int{1, 2}, 2}, + {"TestLoopSlice_All", []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5}, 0}, + {"TestLoopSlice_Empty", []int{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.LoopSlice(c.in, func(i int, val int) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopSlice(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestReverseLoopSlice(t *testing.T) { + var cases = []struct { + name string + in []int + out []int + breakIndex uint + }{ + {"TestReverseLoopSlice_Part", []int{1, 2, 3, 4, 5}, []int{5, 4}, 2}, + {"TestReverseLoopSlice_All", []int{1, 2, 3, 4, 5}, []int{5, 4, 3, 2, 1}, 0}, + {"TestReverseLoopSlice_Empty", []int{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.ReverseLoopSlice(c.in, func(i int, val int) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == uint(len(c.in))-c.breakIndex { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("ReverseLoopSlice(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMap(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out map[int]string + breakIndex uint + }{ + {"TestLoopMap_Part", map[int]string{1: "1", 2: "2", 3: "3"}, map[int]string{1: "1", 2: "2"}, 2}, + {"TestLoopMap_All", map[int]string{1: "1", 2: "2", 3: "3"}, map[int]string{1: "1", 2: "2", 3: "3"}, 0}, + {"TestLoopMap_Empty", map[int]string{}, map[int]string{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result = make(map[int]string) + collection.LoopMap(c.in, func(i int, key int, val string) bool { + result[key] = val + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableMap(result, c.out) { + t.Errorf("LoopMap(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByOrderedKeyAsc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []int + breakIndex uint + }{ + {"TestLoopMapByOrderedKeyAsc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []int{1, 2}, 2}, + {"TestLoopMapByOrderedKeyAsc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []int{1, 2, 3}, 0}, + {"TestLoopMapByOrderedKeyAsc_Empty", map[int]string{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.LoopMapByOrderedKeyAsc(c.in, func(i int, key int, val string) bool { + result = append(result, key) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByOrderedKeyAsc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByOrderedKeyDesc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []int + breakIndex uint + }{ + {"TestLoopMapByOrderedKeyDesc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []int{3, 2}, 2}, + {"TestLoopMapByOrderedKeyDesc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []int{3, 2, 1}, 0}, + {"TestLoopMapByOrderedKeyDesc_Empty", map[int]string{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.LoopMapByOrderedKeyDesc(c.in, func(i int, key int, val string) bool { + result = append(result, key) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByOrderedKeyDesc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByOrderedValueAsc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []string + breakIndex uint + }{ + {"TestLoopMapByOrderedValueAsc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"1", "2"}, 2}, + {"TestLoopMapByOrderedValueAsc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"1", "2", "3"}, 0}, + {"TestLoopMapByOrderedValueAsc_Empty", map[int]string{}, []string{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []string + collection.LoopMapByOrderedValueAsc(c.in, func(i int, key int, val string) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByOrderedValueAsc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByOrderedValueDesc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []string + breakIndex uint + }{ + {"TestLoopMapByOrderedValueDesc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"3", "2"}, 2}, + {"TestLoopMapByOrderedValueDesc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"3", "2", "1"}, 0}, + {"TestLoopMapByOrderedValueDesc_Empty", map[int]string{}, []string{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []string + collection.LoopMapByOrderedValueDesc(c.in, func(i int, key int, val string) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByOrderedValueDesc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByKeyGetterAsc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []int + breakIndex uint + }{ + {"TestLoopMapByKeyGetterAsc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []int{1, 2}, 2}, + {"TestLoopMapByKeyGetterAsc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []int{1, 2, 3}, 0}, + {"TestLoopMapByKeyGetterAsc_Empty", map[int]string{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.LoopMapByKeyGetterAsc(c.in, func(key int) int { + return key + }, func(i int, key int, val string) bool { + result = append(result, key) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByKeyGetterAsc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByKeyGetterDesc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []int + breakIndex uint + }{ + {"TestLoopMapByKeyGetterDesc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []int{3, 2}, 2}, + {"TestLoopMapByKeyGetterDesc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []int{3, 2, 1}, 0}, + {"TestLoopMapByKeyGetterDesc_Empty", map[int]string{}, []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []int + collection.LoopMapByKeyGetterDesc(c.in, func(key int) int { + return key + }, func(i int, key int, val string) bool { + result = append(result, key) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByKeyGetterDesc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByValueGetterAsc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []string + breakIndex uint + }{ + {"TestLoopMapByValueGetterAsc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"1", "2"}, 2}, + {"TestLoopMapByValueGetterAsc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"1", "2", "3"}, 0}, + {"TestLoopMapByValueGetterAsc_Empty", map[int]string{}, []string{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []string + collection.LoopMapByValueGetterAsc(c.in, func(val string) string { + return val + }, func(i int, key int, val string) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByValueGetterAsc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} + +func TestLoopMapByValueGetterDesc(t *testing.T) { + var cases = []struct { + name string + in map[int]string + out []string + breakIndex uint + }{ + {"TestLoopMapByValueGetterDesc_Part", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"3", "2"}, 2}, + {"TestLoopMapByValueGetterDesc_All", map[int]string{1: "1", 2: "2", 3: "3"}, []string{"3", "2", "1"}, 0}, + {"TestLoopMapByValueGetterDesc_Empty", map[int]string{}, []string{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var result []string + collection.LoopMapByValueGetterDesc(c.in, func(val string) string { + return val + }, func(i int, key int, val string) bool { + result = append(result, val) + if c.breakIndex != 0 && uint(i) == c.breakIndex-1 { + return false + } + return true + }) + if !collection.EqualComparableSlice(result, c.out) { + t.Errorf("LoopMapByValueGetterDesc(%v) got %v, want %v", c.in, result, c.out) + } + }) + } +} diff --git a/toolkit/collection/map.go b/toolkit/collection/map.go new file mode 100644 index 0000000..6a8a72b --- /dev/null +++ b/toolkit/collection/map.go @@ -0,0 +1,25 @@ +package collection + +// MappingFromSlice 将切片中的元素进行转换 +func MappingFromSlice[S ~[]V, NS []N, V, N any](slice S, handler func(value V) N) NS { + if slice == nil { + return nil + } + result := make(NS, len(slice)) + for i, v := range slice { + result[i] = handler(v) + } + return result +} + +// MappingFromMap 将 map 中的元素进行转换 +func MappingFromMap[M ~map[K]V, NM map[K]N, K comparable, V, N any](m M, handler func(value V) N) NM { + if m == nil { + return nil + } + result := make(NM, len(m)) + for k, v := range m { + result[k] = handler(v) + } + return result +} diff --git a/toolkit/collection/map_example_test.go b/toolkit/collection/map_example_test.go new file mode 100644 index 0000000..d26b712 --- /dev/null +++ b/toolkit/collection/map_example_test.go @@ -0,0 +1,24 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleMappingFromSlice() { + result := collection.MappingFromSlice([]int{1, 2, 3}, func(value int) int { + return value + 1 + }) + fmt.Println(result) + // Output: + // [2 3 4] +} + +func ExampleMappingFromMap() { + result := collection.MappingFromMap(map[int]int{1: 1, 2: 2, 3: 3}, func(value int) int { + return value + 1 + }) + fmt.Println(result) + // Output: + // map[1:2 2:3 3:4] +} diff --git a/toolkit/collection/map_test.go b/toolkit/collection/map_test.go new file mode 100644 index 0000000..163b598 --- /dev/null +++ b/toolkit/collection/map_test.go @@ -0,0 +1,60 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestMappingFromSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestMappingFromSlice_NonEmptySlice", []int{1, 2, 3}, []int{2, 3, 4}}, + {"TestMappingFromSlice_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MappingFromSlice[[]int, []int](c.input, func(value int) int { + return value + 1 + }) + if len(result) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + for i := 0; i < len(result); i++ { + if result[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the value of input is not equal") + } + } + }) + } +} + +func TestMappingFromMap(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]int + }{ + {"TestMappingFromMap_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]int{1: 2, 2: 3, 3: 4}}, + {"TestMappingFromMap_EmptyMap", map[int]int{}, map[int]int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MappingFromMap[map[int]int, map[int]int](c.input, func(value int) int { + return value + 1 + }) + if len(result) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + for k, v := range result { + if v != c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the value of input is not equal") + } + } + }) + } +} diff --git a/toolkit/collection/mappings/sync_map.go b/toolkit/collection/mappings/sync_map.go new file mode 100644 index 0000000..6da93c0 --- /dev/null +++ b/toolkit/collection/mappings/sync_map.go @@ -0,0 +1,221 @@ +package mappings + +import ( + "encoding/json" + "github.com/kercylan98/minotaur/utils/collection" + "sync" +) + +// NewSyncMap 创建一个 SyncMap +func NewSyncMap[K comparable, V any](source ...map[K]V) *SyncMap[K, V] { + m := &SyncMap[K, V]{ + data: make(map[K]V), + } + if len(source) > 0 { + m.data = collection.MergeMaps(source...) + } + return m +} + +// SyncMap 是基于 sync.RWMutex 实现的线程安全的 map +// - 适用于要考虑并发读写但是并发读写的频率不高的情况 +type SyncMap[K comparable, V any] struct { + lock sync.RWMutex + data map[K]V + atom bool +} + +// Set 设置一个值 +func (sm *SyncMap[K, V]) Set(key K, value V) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + sm.data[key] = value +} + +// Get 获取一个值 +func (sm *SyncMap[K, V]) Get(key K) V { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + return sm.data[key] +} + +// Atom 原子操作 +func (sm *SyncMap[K, V]) Atom(handle func(m map[K]V)) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + handle(sm.data) +} + +// Exist 判断是否存在 +func (sm *SyncMap[K, V]) Exist(key K) bool { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + _, exist := sm.data[key] + return exist +} + +// GetExist 获取一个值并判断是否存在 +func (sm *SyncMap[K, V]) GetExist(key K) (V, bool) { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + value, exist := sm.data[key] + return value, exist +} + +// Delete 删除一个值 +func (sm *SyncMap[K, V]) Delete(key K) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + delete(sm.data, key) +} + +// DeleteGet 删除一个值并返回 +func (sm *SyncMap[K, V]) DeleteGet(key K) V { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + v := sm.data[key] + delete(sm.data, key) + return v +} + +// DeleteGetExist 删除一个值并返回是否存在 +func (sm *SyncMap[K, V]) DeleteGetExist(key K) (V, bool) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + v, exist := sm.data[key] + delete(sm.data, key) + return v, exist +} + +// DeleteExist 删除一个值并返回是否存在 +func (sm *SyncMap[K, V]) DeleteExist(key K) bool { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + if _, exist := sm.data[key]; !exist { + sm.lock.Unlock() + return exist + } + delete(sm.data, key) + return true +} + +// Clear 清空 +func (sm *SyncMap[K, V]) Clear() { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + for k := range sm.data { + delete(sm.data, k) + } +} + +// ClearHandle 清空并处理 +func (sm *SyncMap[K, V]) ClearHandle(handle func(key K, value V)) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + for k, v := range sm.data { + handle(k, v) + delete(sm.data, k) + } +} + +// Range 遍历所有值,如果 handle 返回 true 则停止遍历 +func (sm *SyncMap[K, V]) Range(handle func(key K, value V) bool) { + if !sm.atom { + sm.lock.Lock() + defer sm.lock.Unlock() + } + for k, v := range sm.data { + key, value := k, v + if handle(key, value) { + break + } + } +} + +// Keys 获取所有的键 +func (sm *SyncMap[K, V]) Keys() []K { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + var s = make([]K, 0, len(sm.data)) + for k := range sm.data { + s = append(s, k) + } + return s +} + +// Slice 获取所有的值 +func (sm *SyncMap[K, V]) Slice() []V { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + var s = make([]V, 0, len(sm.data)) + for _, v := range sm.data { + s = append(s, v) + } + return s +} + +// Map 转换为普通 map +func (sm *SyncMap[K, V]) Map() map[K]V { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + var m = make(map[K]V) + for k, v := range sm.data { + m[k] = v + } + return m +} + +// Size 获取数量 +func (sm *SyncMap[K, V]) Size() int { + if !sm.atom { + sm.lock.RLock() + defer sm.lock.RUnlock() + } + return len(sm.data) +} + +func (sm *SyncMap[K, V]) MarshalJSON() ([]byte, error) { + m := sm.Map() + return json.Marshal(m) +} + +func (sm *SyncMap[K, V]) UnmarshalJSON(bytes []byte) error { + var m = make(map[K]V) + if !sm.atom { + sm.lock.Lock() + sm.lock.Unlock() + } + if err := json.Unmarshal(bytes, &m); err != nil { + return err + } + sm.data = m + return nil +} diff --git a/toolkit/collection/merge.go b/toolkit/collection/merge.go new file mode 100644 index 0000000..1d6ee50 --- /dev/null +++ b/toolkit/collection/merge.go @@ -0,0 +1,67 @@ +package collection + +// MergeSlice 合并切片 +func MergeSlice[V any](values ...V) (result []V) { + if len(values) == 0 { + return nil + } + + result = make([]V, 0, len(values)) + result = append(result, values...) + return +} + +// MergeSlices 合并切片 +func MergeSlices[S ~[]V, V any](slices ...S) (result S) { + if len(slices) == 0 { + return nil + } + + var length int + for _, slice := range slices { + length += len(slice) + } + + result = make(S, 0, length) + for _, slice := range slices { + result = append(result, slice...) + } + return +} + +// MergeMaps 合并 map,当多个 map 中存在相同的 key 时,后面的 map 中的 key 将会覆盖前面的 map 中的 key +func MergeMaps[M ~map[K]V, K comparable, V any](maps ...M) (result M) { + if len(maps) == 0 { + return make(M) + } + + var length int + for _, m := range maps { + length += len(m) + } + + result = make(M, length) + for _, m := range maps { + for k, v := range m { + result[k] = v + } + } + return +} + +// MergeMapsWithSkip 合并 map,当多个 map 中存在相同的 key 时,后面的 map 中的 key 将会被跳过 +func MergeMapsWithSkip[M ~map[K]V, K comparable, V any](maps ...M) (result M) { + if len(maps) == 0 { + return nil + } + + result = make(M) + for _, m := range maps { + for k, v := range m { + if _, ok := result[k]; !ok { + result[k] = v + } + } + } + return +} diff --git a/toolkit/collection/merge_example_test.go b/toolkit/collection/merge_example_test.go new file mode 100644 index 0000000..bf34c43 --- /dev/null +++ b/toolkit/collection/merge_example_test.go @@ -0,0 +1,43 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleMergeSlice() { + fmt.Println(collection.MergeSlice(1, 2, 3)) + // Output: + // [1 2 3] +} + +func ExampleMergeSlices() { + fmt.Println( + collection.MergeSlices( + []int{1, 2, 3}, + []int{4, 5, 6}, + ), + ) + // Output: + // [1 2 3 4 5 6] +} + +func ExampleMergeMaps() { + m := collection.MergeMaps( + map[int]int{1: 1, 2: 2, 3: 3}, + map[int]int{4: 4, 5: 5, 6: 6}, + ) + fmt.Println(len(m)) + // Output: + // 6 +} + +func ExampleMergeMapsWithSkip() { + m := collection.MergeMapsWithSkip( + map[int]int{1: 1}, + map[int]int{1: 2}, + ) + fmt.Println(m[1]) + // Output: + // 1 +} diff --git a/toolkit/collection/merge_test.go b/toolkit/collection/merge_test.go new file mode 100644 index 0000000..46ce9be --- /dev/null +++ b/toolkit/collection/merge_test.go @@ -0,0 +1,96 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestMergeSlice(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestMergeSlice_NonEmptySlice", []int{1, 2, 3}, []int{1, 2, 3}}, + {"TestMergeSlice_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MergeSlice(c.input...) + if len(result) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + for i := 0; i < len(result); i++ { + if result[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the value of input is not equal") + } + } + }) + } +} + +func TestMergeSlices(t *testing.T) { + var cases = []struct { + name string + input [][]int + expected []int + }{ + {"TestMergeSlices_NonEmptySlice", [][]int{{1, 2, 3}, {4, 5, 6}}, []int{1, 2, 3, 4, 5, 6}}, + {"TestMergeSlices_EmptySlice", [][]int{{}, {}}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MergeSlices(c.input...) + if len(result) != len(c.expected) { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + for i := 0; i < len(result); i++ { + if result[i] != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the value of input is not equal") + } + } + }) + } +} + +func TestMergeMaps(t *testing.T) { + var cases = []struct { + name string + input []map[int]int + expected int + }{ + {"TestMergeMaps_NonEmptyMap", []map[int]int{{1: 1, 2: 2, 3: 3}, {4: 4, 5: 5, 6: 6}}, 6}, + {"TestMergeMaps_EmptyMap", []map[int]int{{}, {}}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MergeMaps(c.input...) + if len(result) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestMergeMapsWithSkip(t *testing.T) { + var cases = []struct { + name string + input []map[int]int + expected int + }{ + {"TestMergeMapsWithSkip_NonEmptyMap", []map[int]int{{1: 1}, {1: 2}}, 1}, + {"TestMergeMapsWithSkip_EmptyMap", []map[int]int{{}, {}}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.MergeMapsWithSkip(c.input...) + if len(result) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} diff --git a/toolkit/collection/random.go b/toolkit/collection/random.go new file mode 100644 index 0000000..03e43fe --- /dev/null +++ b/toolkit/collection/random.go @@ -0,0 +1,236 @@ +package collection + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/random" +) + +// ChooseRandomSliceElementRepeatN 返回 slice 中的 n 个可重复随机元素 +// - 当 slice 长度为 0 或 n 小于等于 0 时将会返回 nil +func ChooseRandomSliceElementRepeatN[S ~[]V, V any](slice S, n int) (result []V) { + if len(slice) == 0 || n <= 0 { + return + } + result = make([]V, n) + m := len(slice) - 1 + for i := 0; i < n; i++ { + result[i] = slice[random.Int(0, m)] + } + return +} + +// ChooseRandomIndexRepeatN 返回 slice 中的 n 个可重复随机元素的索引 +// - 当 slice 长度为 0 或 n 小于等于 0 时将会返回 nil +func ChooseRandomIndexRepeatN[S ~[]V, V any](slice S, n int) (result []int) { + if len(slice) == 0 || n <= 0 { + return + } + result = make([]int, n) + m := len(slice) - 1 + for i := 0; i < n; i++ { + result[i] = random.Int(0, m) + } + return +} + +// ChooseRandomSliceElement 返回 slice 中随机一个元素,当 slice 长度为 0 时将会得到 V 的零值 +func ChooseRandomSliceElement[S ~[]V, V any](slice S) (v V) { + if len(slice) == 0 { + return + } + return slice[random.Int(0, len(slice)-1)] +} + +// ChooseRandomIndex 返回 slice 中随机一个元素的索引,当 slice 长度为 0 时将会得到 -1 +func ChooseRandomIndex[S ~[]V, V any](slice S) (index int) { + if len(slice) == 0 { + return -1 + } + return random.Int(0, len(slice)-1) +} + +// ChooseRandomSliceElementN 返回 slice 中的 n 个不可重复的随机元素 +// - 当 slice 长度为 0 或 n 大于 slice 长度或小于 0 时将会发生 panic +func ChooseRandomSliceElementN[S ~[]V, V any](slice S, n int) (result []V) { + if len(slice) == 0 || n <= 0 || n > len(slice) { + panic(fmt.Errorf("n is greater than the length of the slice or less than 0, n: %d, length: %d", n, len(slice))) + } + result = make([]V, 0, n) + valid := ConvertSliceToIndexOnlyMap(slice) + for i := range valid { + result = append(result, slice[i]) + if len(result) == n { + break + } + } + return +} + +// ChooseRandomIndexN 获取切片中的 n 个随机元素的索引 +// - 如果 n 大于切片长度或小于 0 时将会发生 panic +func ChooseRandomIndexN[S ~[]V, V any](slice S, n int) (result []int) { + if len(slice) == 0 { + return + } + if n > len(slice) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the input or less than 0, inputN: %d, length: %d", n, len(slice))) + } + result = make([]int, n) + for i := 0; i < n; i++ { + result[i] = random.Int(0, len(slice)-1) + } + return +} + +// ChooseRandomMapKeyRepeatN 获取 map 中的 n 个随机 key,允许重复 +// - 如果 n 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapKeyRepeatN[M ~map[K]V, K comparable, V any](m M, n int) (result []K) { + if m == nil { + return + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result = make([]K, n) + for i := 0; i < n; i++ { + for k := range m { + result[i] = k + break + } + } + return +} + +// ChooseRandomMapValueRepeatN 获取 map 中的 n 个随机 n,允许重复 +// - 如果 n 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapValueRepeatN[M ~map[K]V, K comparable, V any](m M, n int) (result []V) { + if m == nil { + return + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result = make([]V, n) + for i := 0; i < n; i++ { + for _, v := range m { + result[i] = v + break + } + } + return +} + +// ChooseRandomMapKeyAndValueRepeatN 获取 map 中的 n 个随机 key 和 v,允许重复 +// - 如果 n 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapKeyAndValueRepeatN[M ~map[K]V, K comparable, V any](m M, n int) M { + if m == nil { + return nil + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result := make(M, n) + for i := 0; i < n; i++ { + for k, v := range m { + result[k] = v + break + } + } + return result +} + +// ChooseRandomMapKey 获取 map 中的随机 key +func ChooseRandomMapKey[M ~map[K]V, K comparable, V any](m M) (k K) { + if m == nil { + return + } + for k = range m { + return + } + return +} + +// ChooseRandomMapValue 获取 map 中的随机 value +func ChooseRandomMapValue[M ~map[K]V, K comparable, V any](m M) (v V) { + if m == nil { + return + } + for _, v = range m { + return + } + return +} + +// ChooseRandomMapKeyN 获取 map 中的 inputN 个随机 key +// - 如果 inputN 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapKeyN[M ~map[K]V, K comparable, V any](m M, n int) (result []K) { + if m == nil { + return + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result = make([]K, n) + i := 0 + for k := range m { + result[i] = k + i++ + if i == n { + break + } + } + return +} + +// ChooseRandomMapValueN 获取 map 中的 n 个随机 value +// - 如果 n 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapValueN[M ~map[K]V, K comparable, V any](m M, n int) (result []V) { + if m == nil { + return + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result = make([]V, n) + i := 0 + for _, v := range m { + result[i] = v + i++ + if i == n { + break + } + } + return +} + +// ChooseRandomMapKeyAndValue 获取 map 中的随机 key 和 v +func ChooseRandomMapKeyAndValue[M ~map[K]V, K comparable, V any](m M) (k K, v V) { + if m == nil { + return + } + for k, v = range m { + return + } + return +} + +// ChooseRandomMapKeyAndValueN 获取 map 中的 inputN 个随机 key 和 v +// - 如果 n 大于 map 长度或小于 0 时将会发生 panic +func ChooseRandomMapKeyAndValueN[M ~map[K]V, K comparable, V any](m M, n int) M { + if m == nil { + return nil + } + if n > len(m) || n < 0 { + panic(fmt.Errorf("inputN is greater than the length of the map or less than 0, inputN: %d, length: %d", n, len(m))) + } + result := make(M, n) + i := 0 + for k, v := range m { + result[k] = v + i++ + if i == n { + break + } + } + return result +} diff --git a/toolkit/collection/random_example_test.go b/toolkit/collection/random_example_test.go new file mode 100644 index 0000000..d19a7d5 --- /dev/null +++ b/toolkit/collection/random_example_test.go @@ -0,0 +1,111 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" +) + +func ExampleChooseRandomSliceElementRepeatN() { + result := collection.ChooseRandomSliceElementRepeatN([]int{1}, 10) + fmt.Println(result) + // Output: + // [1 1 1 1 1 1 1 1 1 1] +} + +func ExampleChooseRandomIndexRepeatN() { + result := collection.ChooseRandomIndexRepeatN([]int{1}, 10) + fmt.Println(result) + // Output: + // [0 0 0 0 0 0 0 0 0 0] +} + +func ExampleChooseRandomSliceElement() { + result := collection.ChooseRandomSliceElement([]int{1}) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleChooseRandomIndex() { + result := collection.ChooseRandomIndex([]int{1}) + fmt.Println(result) + // Output: + // 0 +} + +func ExampleChooseRandomSliceElementN() { + result := collection.ChooseRandomSliceElementN([]int{1}, 1) + fmt.Println(result) + // Output: + // [1] +} + +func ExampleChooseRandomIndexN() { + result := collection.ChooseRandomIndexN([]int{1}, 1) + fmt.Println(result) + // Output: + // [0] +} + +func ExampleChooseRandomMapKeyRepeatN() { + result := collection.ChooseRandomMapKeyRepeatN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // [1] +} + +func ExampleChooseRandomMapValueRepeatN() { + result := collection.ChooseRandomMapValueRepeatN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // [1] +} + +func ExampleChooseRandomMapKeyAndValueRepeatN() { + result := collection.ChooseRandomMapKeyAndValueRepeatN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // map[1:1] +} + +func ExampleChooseRandomMapKey() { + result := collection.ChooseRandomMapKey(map[int]int{1: 1}) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleChooseRandomMapValue() { + result := collection.ChooseRandomMapValue(map[int]int{1: 1}) + fmt.Println(result) + // Output: + // 1 +} + +func ExampleChooseRandomMapKeyN() { + result := collection.ChooseRandomMapKeyN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // [1] +} + +func ExampleChooseRandomMapValueN() { + result := collection.ChooseRandomMapValueN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // [1] +} + +func ExampleChooseRandomMapKeyAndValue() { + k, v := collection.ChooseRandomMapKeyAndValue(map[int]int{1: 1}) + fmt.Println(k, v) + // Output: + // 1 1 +} + +func ExampleChooseRandomMapKeyAndValueN() { + result := collection.ChooseRandomMapKeyAndValueN(map[int]int{1: 1}, 1) + fmt.Println(result) + // Output: + // map[1:1] +} diff --git a/toolkit/collection/random_test.go b/toolkit/collection/random_test.go new file mode 100644 index 0000000..27a6e1b --- /dev/null +++ b/toolkit/collection/random_test.go @@ -0,0 +1,280 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "testing" +) + +func TestChooseRandomSliceElementRepeatN(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestChooseRandomSliceElementRepeatN_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestChooseRandomSliceElementRepeatN_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomSliceElementRepeatN(c.input, 3) + if len(result) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomIndexRepeatN(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestChooseRandomIndexRepeatN_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestChooseRandomIndexRepeatN_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomIndexRepeatN(c.input, 3) + if len(result) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomSliceElement(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]bool + }{ + {"TestChooseRandomSliceElement_NonEmptySlice", []int{1, 2, 3}, map[int]bool{1: true, 2: true, 3: true}}, + {"TestChooseRandomSliceElement_EmptySlice", []int{}, map[int]bool{0: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomSliceElement(c.input) + if !c.expected[result] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomIndex(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]bool + }{ + {"TestChooseRandomIndex_NonEmptySlice", []int{1, 2, 3}, map[int]bool{0: true, 1: true, 2: true}}, + {"TestChooseRandomIndex_EmptySlice", []int{}, map[int]bool{-1: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomIndex(c.input) + if !c.expected[result] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomSliceElementN(t *testing.T) { + var cases = []struct { + name string + input []int + }{ + {"TestChooseRandomSliceElementN_NonEmptySlice", []int{1, 2, 3}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := collection.ChooseRandomSliceElementN(c.input, 3) + if !collection.AllInComparableSlice(actual, c.input) { + t.Fatalf("%s failed, actual: %v, error: %s", c.name, actual, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomIndexN(t *testing.T) { + var cases = []struct { + name string + input []int + expected map[int]bool + }{ + {"TestChooseRandomIndexN_NonEmptySlice", []int{1, 2, 3}, map[int]bool{0: true, 1: true, 2: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomIndexN(c.input, 3) + if !c.expected[result[0]] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapKeyRepeatN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapKeyRepeatN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapKeyRepeatN(c.input, 3) + if !c.expected[result[0]] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapValueRepeatN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapValueRepeatN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapValueRepeatN(c.input, 3) + if !c.expected[result[0]] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapKeyAndValueRepeatN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapKeyAndValueRepeatN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapKeyAndValueRepeatN(c.input, 3) + if !c.expected[result[1]] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapKey(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapKey_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + {"TestChooseRandomMapKey_EmptyMap", map[int]int{}, map[int]bool{0: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapKey(c.input) + if !c.expected[result] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapValue(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapValue_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + {"TestChooseRandomMapValue_EmptyMap", map[int]int{}, map[int]bool{0: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapValue(c.input) + if !c.expected[result] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapValueN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapValueN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ChooseRandomMapValueN(c.input, 3) + if !c.expected[result[0]] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, result, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapKeyAndValue(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapKeyAndValue_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + {"TestChooseRandomMapKeyAndValue_EmptyMap", map[int]int{}, map[int]bool{0: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + k, v := collection.ChooseRandomMapKeyAndValue(c.input) + if !c.expected[k] || !c.expected[v] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, k, "the length of input is not equal") + } + }) + } +} + +func TestChooseRandomMapKeyAndValueN(t *testing.T) { + var cases = []struct { + name string + input map[int]int + expected map[int]bool + }{ + {"TestChooseRandomMapKeyAndValueN_NonEmptyMap", map[int]int{1: 1, 2: 2, 3: 3}, map[int]bool{1: true, 2: true, 3: true}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + kvm := collection.ChooseRandomMapKeyAndValueN(c.input, 1) + for k := range kvm { + if !c.expected[k] { + t.Fatalf("%s failed, expected: %v, actual: %v, error: %s", c.name, c.expected, k, "the length of input is not equal") + } + } + }) + } +} diff --git a/toolkit/collection/sort.go b/toolkit/collection/sort.go new file mode 100644 index 0000000..a8bb183 --- /dev/null +++ b/toolkit/collection/sort.go @@ -0,0 +1,60 @@ +package collection + +import ( + "github.com/kercylan98/minotaur/utils/generic" + "github.com/kercylan98/minotaur/utils/random" + "sort" +) + +// DescBy 返回降序比较结果 +func DescBy[Sort generic.Ordered](a, b Sort) bool { + return a > b +} + +// AscBy 返回升序比较结果 +func AscBy[Sort generic.Ordered](a, b Sort) bool { + return a < b +} + +// Desc 对切片进行降序排序 +func Desc[S ~[]V, V any, Sort generic.Ordered](slice *S, getter func(index int) Sort) { + sort.Slice(*slice, func(i, j int) bool { + return getter(i) > getter(j) + }) +} + +// DescByClone 对切片进行降序排序,返回排序后的切片 +func DescByClone[S ~[]V, V any, Sort generic.Ordered](slice S, getter func(index int) Sort) S { + result := CloneSlice(slice) + Desc(&result, getter) + return result + +} + +// Asc 对切片进行升序排序 +func Asc[S ~[]V, V any, Sort generic.Ordered](slice *S, getter func(index int) Sort) { + sort.Slice(*slice, func(i, j int) bool { + return getter(i) < getter(j) + }) +} + +// AscByClone 对切片进行升序排序,返回排序后的切片 +func AscByClone[S ~[]V, V any, Sort generic.Ordered](slice S, getter func(index int) Sort) S { + result := CloneSlice(slice) + Asc(&result, getter) + return result +} + +// Shuffle 对切片进行随机排序 +func Shuffle[S ~[]V, V any](slice *S) { + sort.Slice(*slice, func(i, j int) bool { + return random.Int(0, 1) == 0 + }) +} + +// ShuffleByClone 对切片进行随机排序,返回排序后的切片 +func ShuffleByClone[S ~[]V, V any](slice S) S { + result := CloneSlice(slice) + Shuffle(&result) + return result +} diff --git a/toolkit/collection/sort_example_test.go b/toolkit/collection/sort_example_test.go new file mode 100644 index 0000000..be6164d --- /dev/null +++ b/toolkit/collection/sort_example_test.go @@ -0,0 +1,83 @@ +package collection_test + +import ( + "fmt" + "github.com/kercylan98/minotaur/utils/collection" + "sort" +) + +func ExampleDescBy() { + var slice = []int{1, 2, 3} + sort.Slice(slice, func(i, j int) bool { + return collection.DescBy(slice[i], slice[j]) + }) + fmt.Println(slice) + // Output: + // [3 2 1] +} + +func ExampleAscBy() { + var slice = []int{1, 2, 3} + sort.Slice(slice, func(i, j int) bool { + return collection.AscBy(slice[i], slice[j]) + }) + fmt.Println(slice) + // Output: + // [1 2 3] +} + +func ExampleDesc() { + var slice = []int{1, 2, 3} + collection.Desc(&slice, func(index int) int { + return slice[index] + }) + fmt.Println(slice) + // Output: + // [3 2 1] +} + +func ExampleDescByClone() { + var slice = []int{1, 2, 3} + result := collection.DescByClone(slice, func(index int) int { + return slice[index] + }) + fmt.Println(result) + // Output: + // [3 2 1] +} + +func ExampleAsc() { + var slice = []int{1, 2, 3} + collection.Asc(&slice, func(index int) int { + return slice[index] + }) + fmt.Println(slice) + // Output: + // [1 2 3] +} + +func ExampleAscByClone() { + var slice = []int{1, 2, 3} + result := collection.AscByClone(slice, func(index int) int { + return slice[index] + }) + fmt.Println(result) + // Output: + // [1 2 3] +} + +func ExampleShuffle() { + var slice = []int{1, 2, 3} + collection.Shuffle(&slice) + fmt.Println(len(slice)) + // Output: + // 3 +} + +func ExampleShuffleByClone() { + var slice = []int{1, 2, 3} + result := collection.ShuffleByClone(slice) + fmt.Println(len(result)) + // Output: + // 3 +} diff --git a/toolkit/collection/sort_test.go b/toolkit/collection/sort_test.go new file mode 100644 index 0000000..26a0dfc --- /dev/null +++ b/toolkit/collection/sort_test.go @@ -0,0 +1,191 @@ +package collection_test + +import ( + "github.com/kercylan98/minotaur/utils/collection" + "sort" + "testing" +) + +func TestDescBy(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestDescBy_NonEmptySlice", []int{1, 2, 3}, []int{3, 2, 1}}, + {"TestDescBy_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + sort.Slice(c.input, func(i, j int) bool { + return collection.DescBy(c.input[i], c.input[j]) + }) + for i, v := range c.input { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, c.input) + } + } + }) + } +} + +func TestAscBy(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestAscBy_NonEmptySlice", []int{1, 2, 3}, []int{1, 2, 3}}, + {"TestAscBy_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + sort.Slice(c.input, func(i, j int) bool { + return collection.AscBy(c.input[i], c.input[j]) + }) + for i, v := range c.input { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, c.input) + } + } + }) + } +} + +func TestDesc(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestDesc_NonEmptySlice", []int{1, 2, 3}, []int{3, 2, 1}}, + {"TestDesc_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.Desc(&c.input, func(index int) int { + return c.input[index] + }) + for i, v := range c.input { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, c.input) + } + } + }) + } +} + +func TestDescByClone(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestDescByClone_NonEmptySlice", []int{1, 2, 3}, []int{3, 2, 1}}, + {"TestDescByClone_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.DescByClone(c.input, func(index int) int { + return c.input[index] + }) + for i, v := range result { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, result) + } + } + }) + } +} + +func TestAsc(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestAsc_NonEmptySlice", []int{1, 2, 3}, []int{1, 2, 3}}, + {"TestAsc_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.Asc(&c.input, func(index int) int { + return c.input[index] + }) + for i, v := range c.input { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, c.input) + } + } + }) + } +} + +func TestAscByClone(t *testing.T) { + var cases = []struct { + name string + input []int + expected []int + }{ + {"TestAscByClone_NonEmptySlice", []int{1, 2, 3}, []int{1, 2, 3}}, + {"TestAscByClone_EmptySlice", []int{}, []int{}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.AscByClone(c.input, func(index int) int { + return c.input[index] + }) + for i, v := range result { + if v != c.expected[i] { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, result) + } + } + }) + } +} + +func TestShuffle(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestShuffle_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestShuffle_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + collection.Shuffle(&c.input) + if len(c.input) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, c.input) + } + }) + } +} + +func TestShuffleByClone(t *testing.T) { + var cases = []struct { + name string + input []int + expected int + }{ + {"TestShuffleByClone_NonEmptySlice", []int{1, 2, 3}, 3}, + {"TestShuffleByClone_EmptySlice", []int{}, 0}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := collection.ShuffleByClone(c.input) + if len(result) != c.expected { + t.Fatalf("%s failed, expected: %v, actual: %v", c.name, c.expected, result) + } + }) + } +} diff --git a/server/internal/v2/loadbalancer/consistent_hash.go b/toolkit/loadbalancer/consistent_hash.go similarity index 100% rename from server/internal/v2/loadbalancer/consistent_hash.go rename to toolkit/loadbalancer/consistent_hash.go diff --git a/server/internal/v2/loadbalancer/round_robin.go b/toolkit/loadbalancer/round_robin.go similarity index 93% rename from server/internal/v2/loadbalancer/round_robin.go rename to toolkit/loadbalancer/round_robin.go index b37b24f..a419f9a 100644 --- a/server/internal/v2/loadbalancer/round_robin.go +++ b/toolkit/loadbalancer/round_robin.go @@ -49,7 +49,7 @@ func (r *RoundRobin[Id, T]) Remove(t T) { prev := r.head for i := 0; i < r.size; i++ { - if prev.Next.Value.Id() == t.Id() { + if prev.Next.Value.GetId() == t.GetId() { if prev.Next == r.curr { r.curr = prev } @@ -87,7 +87,7 @@ func (r *RoundRobin[Id, T]) Refresh() { curr := r.head for i := 0; i < r.size; i++ { - if curr.Value.Id() == r.curr.Value.Id() { + if curr.Value.GetId() == r.curr.Value.GetId() { r.curr = curr return } diff --git a/server/internal/v2/loadbalancer/round_robin_item.go b/toolkit/loadbalancer/round_robin_item.go similarity index 63% rename from server/internal/v2/loadbalancer/round_robin_item.go rename to toolkit/loadbalancer/round_robin_item.go index 50c971d..5c9ed1f 100644 --- a/server/internal/v2/loadbalancer/round_robin_item.go +++ b/toolkit/loadbalancer/round_robin_item.go @@ -1,6 +1,6 @@ package loadbalancer type RoundRobinItem[Id comparable] interface { - // Id 返回唯一标识 - Id() Id + // GetId 返回唯一标识 + GetId() Id } diff --git a/toolkit/message/broker.go b/toolkit/message/broker.go new file mode 100644 index 0000000..6d67962 --- /dev/null +++ b/toolkit/message/broker.go @@ -0,0 +1,8 @@ +package message + +// Broker 消息核心的接口定义 +type Broker[I, T comparable] interface { + Run() + Close() + Publish(topic T, event Event[I, T]) error +} diff --git a/toolkit/message/brokers/sparse_goroutine.go b/toolkit/message/brokers/sparse_goroutine.go new file mode 100644 index 0000000..be5a3f0 --- /dev/null +++ b/toolkit/message/brokers/sparse_goroutine.go @@ -0,0 +1,147 @@ +package brokers + +import ( + "context" + "fmt" + "github.com/kercylan98/minotaur/toolkit/loadbalancer" + "github.com/kercylan98/minotaur/toolkit/message" + "runtime" + "sync" + "sync/atomic" +) + +const ( + sparseGoroutineStatusNone = iota - 1 // 未运行 + sparseGoroutineStatusRunning // 运行中 + sparseGoroutineStatusClosing // 关闭中 + sparseGoroutineStatusClosed // 已关闭 +) + +type ( + SparseGoroutineMessageHandler func(handler message.EventExecutor) +) + +func NewSparseGoroutine[I, T comparable](queueFactory func(index int) message.Queue[I, T], handler SparseGoroutineMessageHandler) message.Broker[I, T] { + s := &SparseGoroutine[I, T]{ + lb: loadbalancer.NewRoundRobin[I, message.Queue[I, T]](), + queues: make(map[I]message.Queue[I, T]), + state: sparseGoroutineStatusNone, + location: make(map[T]I), + handler: handler, + queueFactory: queueFactory, + } + defaultNum := runtime.NumCPU() + s.queueRW.Lock() + for i := 0; i < defaultNum; i++ { + queue := s.queueFactory(i) + s.lb.Add(queue) // 运行前添加到负载均衡器,未运行时允许接收消息 + queueId := queue.GetId() + if _, exist := s.queues[queueId]; exist { + panic(fmt.Errorf("duplicate queue id: %v", queueId)) + } + s.queues[queueId] = queue + } + s.queueRW.Unlock() + + return s +} + +type SparseGoroutine[I, T comparable] struct { + state int32 // 状态 + queueSize int // 队列管道大小 + queueBufferSize int // 队列缓冲区大小 + queues map[I]message.Queue[I, T] // 所有使用的队列 + queueRW sync.RWMutex // 队列读写锁 + location map[T]I // Topic 所在队列 Id 映射 + locationRW sync.RWMutex // 所在队列 ID 映射锁 + lb *loadbalancer.RoundRobin[I, message.Queue[I, T]] // 负载均衡器 + wg sync.WaitGroup // 等待组 + handler SparseGoroutineMessageHandler // 消息处理器 + + queueFactory func(index int) message.Queue[I, T] +} + +// Run 启动 Reactor,运行队列 +func (s *SparseGoroutine[I, T]) Run() { + if !atomic.CompareAndSwapInt32(&s.state, sparseGoroutineStatusNone, sparseGoroutineStatusRunning) { + return + } + s.queueRW.Lock() + queues := s.queues + for _, queue := range queues { + s.wg.Add(1) + go queue.Run() + + go func(r *SparseGoroutine[I, T], queue message.Queue[I, T]) { + defer r.wg.Done() + for h := range queue.Consume() { + h.Exec( + // onProcess + func(topic T, event message.EventExecutor) { + s.handler(event) + }, + // onFinish + func(topic T, last bool) { + if !last { + return + } + s.locationRW.Lock() + defer s.locationRW.Unlock() + delete(s.location, topic) + }, + ) + } + }(s, queue) + } + s.queueRW.Unlock() + s.wg.Wait() +} + +func (s *SparseGoroutine[I, T]) Close() { + if !atomic.CompareAndSwapInt32(&s.state, sparseGoroutineStatusRunning, sparseGoroutineStatusClosing) { + return + } + s.queueRW.Lock() + var wg sync.WaitGroup + wg.Add(len(s.queues)) + for _, queue := range s.queues { + go func(queue message.Queue[I, T]) { + defer wg.Done() + queue.Close() + }(queue) + } + wg.Wait() + atomic.StoreInt32(&s.state, sparseGoroutineStatusClosed) + s.queueRW.Unlock() +} + +// Publish 将消息分发到特定 topic,当 topic 首次使用时,将会根据负载均衡策略选择一个队列 +// - 设置 count 会增加消息的外部计数,当 SparseGoroutine 关闭时会等待外部计数归零 +func (s *SparseGoroutine[I, T]) Publish(topic T, event message.Event[I, T]) error { + s.queueRW.RLock() + if atomic.LoadInt32(&s.state) > sparseGoroutineStatusClosing { + s.queueRW.RUnlock() + return fmt.Errorf("broker closing or closed") + } + + var next message.Queue[I, T] + s.locationRW.RLock() + i, exist := s.location[topic] + s.locationRW.RUnlock() + if !exist { + s.locationRW.Lock() + if i, exist = s.location[topic]; !exist { + next = s.lb.Next() + s.location[topic] = next.GetId() + } else { + next = s.queues[i] + } + s.locationRW.Unlock() + } else { + next = s.queues[i] + } + s.queueRW.RUnlock() + + event.OnInitialize(context.Background(), s) + return next.Publish(topic, event) +} diff --git a/toolkit/message/event.go b/toolkit/message/event.go new file mode 100644 index 0000000..4b69df8 --- /dev/null +++ b/toolkit/message/event.go @@ -0,0 +1,36 @@ +package message + +import ( + "context" + "time" +) + +type Event[I, T comparable] interface { + // OnInitialize 消息初始化 + OnInitialize(ctx context.Context, broker Broker[I, T]) + + // OnPublished 消息发布成功 + OnPublished(topic T, queue Queue[I, T]) + + // OnProcess 消息开始处理 + OnProcess(topic T, queue Queue[I, T], startAt time.Time) + + // OnProcessed 消息处理完成 + OnProcessed(topic T, queue Queue[I, T], endAt time.Time) +} + +type EventExecutor func() +type EventHandler[T comparable] func(topic T, event EventExecutor) +type EventFinisher[I, T comparable] func(topic T, last bool) + +type EventInfo[I, T comparable] interface { + GetTopic() T + Exec( + handler EventHandler[T], + finisher EventFinisher[I, T], + ) +} + +func (e EventExecutor) Exec() { + e() +} diff --git a/toolkit/message/events/asynchronous.go b/toolkit/message/events/asynchronous.go new file mode 100644 index 0000000..608bb29 --- /dev/null +++ b/toolkit/message/events/asynchronous.go @@ -0,0 +1,79 @@ +package events + +import ( + "context" + "github.com/kercylan98/minotaur/toolkit/message" + "time" +) + +type ( + AsynchronousActuator func(context.Context, func(context.Context)) // 负责执行异步消息的执行器 + AsynchronousHandler func(context.Context) error // 异步消息逻辑处理器 + AsynchronousCallbackHandler func(context.Context, error) // 异步消息回调处理器 +) + +// Asynchronous 创建一个异步消息实例,并指定相应的处理器。 +// 该函数接收以下参数: +// - broker:消息所属的 Broker 实例。 +// - actuator:异步消息的执行器,负责执行异步消息的逻辑,当该参数为空时,将会使用默认的 go func()。 +// - handler:异步消息的逻辑处理器,用于执行实际的异步消息处理逻辑,可选参数。 +// - callback:异步消息的回调处理器,处理消息处理完成后的回调操作,可选参数。 +// - afterHandler:异步消息执行完成后的处理器,用于进行后续的处理操作,可选参数。 +// +// 该函数除了 handler,其他所有处理器均为同步执行 +// +// 返回值为一个实现了 Event 接口的异步消息实例。 +func Asynchronous[I, T comparable]( + actuator AsynchronousActuator, + handler AsynchronousHandler, + callback AsynchronousCallbackHandler, +) message.Event[I, T] { + m := &asynchronous[I, T]{ + actuator: actuator, + handler: handler, + callback: callback, + } + if m.actuator == nil { + m.actuator = func(ctx context.Context, f func(context.Context)) { + go f(ctx) + } + } + + return m +} + +type asynchronous[I, T comparable] struct { + ctx context.Context + broker message.Broker[I, T] + actuator AsynchronousActuator + handler AsynchronousHandler + callback AsynchronousCallbackHandler +} + +func (s *asynchronous[I, T]) OnInitialize(ctx context.Context, broker message.Broker[I, T]) { + s.ctx = ctx + s.broker = broker +} + +func (s *asynchronous[I, T]) OnPublished(topic T, queue message.Queue[I, T]) { + queue.IncrementCustomMessageCount(topic, 1) +} + +func (s *asynchronous[I, T]) OnProcess(topic T, queue message.Queue[I, T], startAt time.Time) { + s.actuator(s.ctx, func(ctx context.Context) { + var err error + if s.handler != nil { + err = s.handler(s.ctx) + } + + s.broker.Publish(topic, Synchronous[I, T](func(ctx context.Context) { + if s.callback != nil { + s.callback(ctx, err) + } + })) + }) +} + +func (s *asynchronous[I, T]) OnProcessed(topic T, queue message.Queue[I, T], endAt time.Time) { + queue.IncrementCustomMessageCount(topic, -1) +} diff --git a/toolkit/message/events/synchronous.go b/toolkit/message/events/synchronous.go new file mode 100644 index 0000000..9d48e40 --- /dev/null +++ b/toolkit/message/events/synchronous.go @@ -0,0 +1,37 @@ +package events + +import ( + "context" + "github.com/kercylan98/minotaur/toolkit/message" + "time" +) + +type ( + SynchronousHandler func(context.Context) +) + +func Synchronous[I, T comparable](handler SynchronousHandler) message.Event[I, T] { + return &synchronous[I, T]{ + handler: handler, + } +} + +type synchronous[I, T comparable] struct { + ctx context.Context + handler SynchronousHandler +} + +func (s *synchronous[I, T]) OnInitialize(ctx context.Context, broker message.Broker[I, T]) { + s.ctx = ctx +} + +func (s *synchronous[I, T]) OnPublished(topic T, queue message.Queue[I, T]) { + +} + +func (s *synchronous[I, T]) OnProcess(topic T, queue message.Queue[I, T], startAt time.Time) { + s.handler(s.ctx) +} + +func (s *synchronous[I, T]) OnProcessed(topic T, queue message.Queue[I, T], endAt time.Time) { +} diff --git a/toolkit/message/producer.go b/toolkit/message/producer.go new file mode 100644 index 0000000..3396efd --- /dev/null +++ b/toolkit/message/producer.go @@ -0,0 +1,5 @@ +package message + +type Producer[T comparable] interface { + GetTopic() T +} diff --git a/toolkit/message/queue.go b/toolkit/message/queue.go new file mode 100644 index 0000000..da38f3b --- /dev/null +++ b/toolkit/message/queue.go @@ -0,0 +1,16 @@ +package message + +type Queue[I, T comparable] interface { + // GetId 获取队列 Id + GetId() I + // Publish 向队列中推送消息 + Publish(topic T, event Event[I, T]) error + // IncrementCustomMessageCount 增加自定义消息计数 + IncrementCustomMessageCount(topic T, delta int64) + // Run 运行队列 + Run() + // Consume 消费消息 + Consume() <-chan EventInfo[I, T] + // Close 关闭队列 + Close() +} diff --git a/toolkit/message/queues/non_blocking_rw.go b/toolkit/message/queues/non_blocking_rw.go new file mode 100644 index 0000000..5c9ce5e --- /dev/null +++ b/toolkit/message/queues/non_blocking_rw.go @@ -0,0 +1,197 @@ +package queues + +import ( + "errors" + "github.com/kercylan98/minotaur/toolkit/message" + "github.com/kercylan98/minotaur/utils/buffer" + "sync" + "sync/atomic" + "time" +) + +const ( + NonBlockingRWStatusNone NonBlockingRWState = iota - 1 // 队列未运行 + NonBlockingRWStatusRunning // 队列运行中 + NonBlockingRWStatusClosing // 队列关闭中 + NonBlockingRWStatusClosed // 队列已关闭 +) + +var ( + ErrorQueueClosed = errors.New("queue closed") // 队列已关闭 + ErrorQueueInvalid = errors.New("queue invalid") // 无效的队列 +) + +type NonBlockingRWState = int32 + +type nonBlockingRWEventInfo[I, T comparable] struct { + topic T + event message.Event[I, T] + exec func(handler message.EventHandler[T], finisher message.EventFinisher[I, T]) +} + +func (e *nonBlockingRWEventInfo[I, T]) GetTopic() T { + return e.topic +} + +func (e *nonBlockingRWEventInfo[I, T]) Exec(handler message.EventHandler[T], finisher message.EventFinisher[I, T]) { + e.exec(handler, finisher) +} + +// NewNonBlockingRW 创建一个并发安全的队列 NonBlockingRW,该队列支持通过 chanSize 自定义读取 channel 的大小,同支持使用 bufferSize 指定 buffer.Ring 的初始大小 +// - closedHandler 可选的设置队列关闭处理函数,在队列关闭后将执行该函数。此刻队列不再可用 +func NewNonBlockingRW[I, T comparable](id I, chanSize, bufferSize int) message.Queue[I, T] { + q := &NonBlockingRW[I, T]{ + id: id, + status: NonBlockingRWStatusNone, + c: make(chan message.EventInfo[I, T], chanSize), + buf: buffer.NewRing[nonBlockingRWEventInfo[I, T]](bufferSize), + condRW: &sync.RWMutex{}, + topics: make(map[T]int64), + } + q.cond = sync.NewCond(q.condRW) + return q +} + +// NonBlockingRW 队列是一个适用于消息处理等场景的并发安全的数据结构 +// - 该队列接收自定义的消息 M,并将消息有序的传入 Read 函数所返回的 channel 中以供处理 +// - 该结构主要实现目标为读写分离且并发安全的非阻塞传输队列,当消费阻塞时以牺牲内存为代价换取消息的生产不阻塞,适用于服务器消息处理等 +// - 该队列保证了消息的完整性,确保消息不丢失,在队列关闭后会等待所有消息处理完毕后进行关闭,并提供 SetClosedHandler 函数来监听队列的关闭信号 +type NonBlockingRW[I, T comparable] struct { + id I // 队列 ID + status int32 // 状态标志 + total int64 // 消息总计数 + topics map[T]int64 // 主题对应的消息计数映射 + buf *buffer.Ring[nonBlockingRWEventInfo[I, T]] // 消息缓冲区 + c chan message.EventInfo[I, T] // 消息读取通道 + cs chan struct{} // 关闭信号 + cond *sync.Cond // 条件变量 + condRW *sync.RWMutex // 条件变量的读写锁 +} + +// GetId 获取队列 Id +func (n *NonBlockingRW[I, T]) GetId() I { + return n.id +} + +// Run 阻塞的运行队列,当队列非首次运行时,将会引发来自 ErrorQueueInvalid 的 panic +func (n *NonBlockingRW[I, T]) Run() { + if atomic.LoadInt32(&n.status) != NonBlockingRWStatusNone { + panic(ErrorQueueInvalid) + } + atomic.StoreInt32(&n.status, NonBlockingRWStatusRunning) + for { + n.cond.L.Lock() + for n.buf.IsEmpty() { + if atomic.LoadInt32(&n.status) >= NonBlockingRWStatusClosing && n.total == 0 { + n.cond.L.Unlock() + atomic.StoreInt32(&n.status, NonBlockingRWStatusClosed) + close(n.c) + close(n.cs) + return + } + n.cond.Wait() + } + items := n.buf.ReadAll() + n.cond.L.Unlock() + for i := 0; i < len(items); i++ { + ei := &items[i] + n.c <- ei + } + } +} + +// Consume 获取队列消息的只读通道, +func (n *NonBlockingRW[I, T]) Consume() <-chan message.EventInfo[I, T] { + return n.c +} + +// Close 关闭队列 +func (n *NonBlockingRW[I, T]) Close() { + if atomic.CompareAndSwapInt32(&n.status, NonBlockingRWStatusRunning, NonBlockingRWStatusClosing) { + n.cs = make(chan struct{}) + n.cond.Broadcast() + <-n.cs + } +} + +// GetMessageCount 获取消息数量 +func (n *NonBlockingRW[I, T]) GetMessageCount() (count int64) { + n.condRW.RLock() + defer n.condRW.RUnlock() + return n.total +} + +// GetTopicMessageCount 获取特定主题的消息数量 +func (n *NonBlockingRW[I, T]) GetTopicMessageCount(topic T) int64 { + n.condRW.RLock() + defer n.condRW.RUnlock() + return n.topics[topic] +} + +func (n *NonBlockingRW[I, T]) Publish(topic T, event message.Event[I, T]) error { + if atomic.LoadInt32(&n.status) > NonBlockingRWStatusClosing { + return ErrorQueueClosed + } + + ei := nonBlockingRWEventInfo[I, T]{ + topic: topic, + event: event, + exec: func(handler message.EventHandler[T], finisher message.EventFinisher[I, T]) { + defer func() { + event.OnProcessed(topic, n, time.Now()) + + n.cond.L.Lock() + n.total-- + curr := n.topics[topic] - 1 + if curr != 0 { + n.topics[topic] = curr + } else { + delete(n.topics, topic) + } + if finisher != nil { + finisher(topic, curr == 0) + } + n.cond.Signal() + n.cond.L.Unlock() + }() + + handler(topic, func() { + event.OnProcess(topic, n, time.Now()) + }) + return + }, + } + + n.cond.L.Lock() + n.topics[topic]++ + n.total++ + n.buf.Write(ei) + //log.Info("消息总计数", log.Int64("计数", q.state.total)) + n.cond.Signal() + n.cond.L.Unlock() + + return nil +} + +func (n *NonBlockingRW[I, T]) IncrementCustomMessageCount(topic T, delta int64) { + n.cond.L.Lock() + n.total += delta + n.topics[topic] += delta + n.cond.Signal() + n.cond.L.Unlock() +} + +// IsClosed 判断队列是否已关闭 +func (n *NonBlockingRW[I, T]) IsClosed() bool { + return atomic.LoadInt32(&n.status) == NonBlockingRWStatusClosed +} + +// IsClosing 判断队列是否正在关闭 +func (n *NonBlockingRW[I, T]) IsClosing() bool { + return atomic.LoadInt32(&n.status) == NonBlockingRWStatusClosing +} + +// IsRunning 判断队列是否正在运行 +func (n *NonBlockingRW[I, T]) IsRunning() bool { + return atomic.LoadInt32(&n.status) == NonBlockingRWStatusRunning +} diff --git a/utils/generator/astgo/README.md b/utils/generator/astgo/README.md deleted file mode 100644 index 3433548..0000000 --- a/utils/generator/astgo/README.md +++ /dev/null @@ -1,197 +0,0 @@ -# Astgo - -[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur) -![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat) - -暂无介绍... - - -## 目录导航 -列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️ -
-展开 / 折叠目录导航 - - -> 包级函数定义 - -|函数名称|描述 -|:--|:-- -|[NewPackage](#NewPackage)|暂无描述... - - -> 类型定义 - -|类型|名称|描述 -|:--|:--|:-- -|`STRUCT`|[Comment](#struct_Comment)|暂无描述... -|`STRUCT`|[Field](#struct_Field)|暂无描述... -|`STRUCT`|[File](#struct_File)|暂无描述... -|`STRUCT`|[Function](#struct_Function)|暂无描述... -|`STRUCT`|[Package](#struct_Package)|暂无描述... -|`STRUCT`|[Struct](#struct_Struct)|暂无描述... -|`STRUCT`|[Type](#struct_Type)|暂无描述... - -
- - -*** -## 详情信息 -#### func NewPackage(dir string) (*Package, error) - - -
-查看 / 收起单元测试 - - -```go - -func TestNewPackage(t *testing.T) { - p, err := astgo.NewPackage(`/Users/kercylan/Coding.localized/Go/minotaur/server`) - if err != nil { - panic(err) - } - fmt.Println(string(super.MarshalIndentJSON(p, "", " "))) -} - -``` - - -
- - -*** - -### Comment `STRUCT` - -```go -type Comment struct { - Comments []string - Clear []string -} -``` - -### Field `STRUCT` - -```go -type Field struct { - Anonymous bool - Name string - Type *Type - Comments *Comment -} -``` - -### File `STRUCT` - -```go -type File struct { - af *ast.File - owner *Package - FilePath string - Structs []*Struct - Functions []*Function - Comment *Comment -} -``` - - -#### func (*File) Package() string - -*** - -### Function `STRUCT` - -```go -type Function struct { - decl *ast.FuncDecl - Name string - Internal bool - Generic []*Field - Params []*Field - Results []*Field - Comments *Comment - Struct *Field - IsExample bool - IsTest bool - IsBenchmark bool - Test bool -} -``` - - -#### func (*Function) Code() string - -*** - -### Package `STRUCT` - -```go -type Package struct { - Dir string - Name string - Dirs []string - Files []*File - Functions map[string]*Function -} -``` - - -#### func (*Package) StructFunc(name string) []*Function - -*** - - -#### func (*Package) PackageFunc() []*Function - -*** - - -#### func (*Package) Structs() []*Struct - -*** - - -#### func (*Package) FileComments() *Comment - -*** - - -#### func (*Package) GetUnitTest(f *Function) *Function - -*** - - -#### func (*Package) GetExampleTest(f *Function) *Function - -*** - - -#### func (*Package) GetBenchmarkTest(f *Function) *Function - -*** - -### Struct `STRUCT` - -```go -type Struct struct { - Name string - Internal bool - Interface bool - Comments *Comment - Generic []*Field - Fields []*Field - Type *Type - Test bool -} -``` - -### Type `STRUCT` - -```go -type Type struct { - expr ast.Expr - Sign string - IsPointer bool - Name string -} -``` diff --git a/utils/generator/astgo/comment.go b/utils/generator/astgo/comment.go deleted file mode 100644 index 9e7527c..0000000 --- a/utils/generator/astgo/comment.go +++ /dev/null @@ -1,31 +0,0 @@ -package astgo - -import ( - "go/ast" - "strings" -) - -func newComment(name string, cg *ast.CommentGroup) *Comment { - c := &Comment{} - if cg == nil { - return c - } - for i, comment := range cg.List { - c.Comments = append(c.Comments, comment.Text) - cc := strings.TrimPrefix(strings.Replace(comment.Text, "// ", "//", 1), "//") - if i == 0 { - tsc := strings.TrimSpace(cc) - if strings.HasPrefix(tsc, name) { - s := strings.TrimSpace(strings.TrimPrefix(tsc, name)) - cc = s - } - } - c.Clear = append(c.Clear, cc) - } - return c -} - -type Comment struct { - Comments []string - Clear []string -} diff --git a/utils/generator/astgo/field.go b/utils/generator/astgo/field.go deleted file mode 100644 index da2a5e2..0000000 --- a/utils/generator/astgo/field.go +++ /dev/null @@ -1,37 +0,0 @@ -package astgo - -import ( - "fmt" - "go/ast" -) - -func newField(field *ast.Field) []*Field { - if len(field.Names) == 0 { - return []*Field{{ - Anonymous: true, - Type: newType(field.Type), - Comments: newComment("", field.Comment), - }} - } else { - var fs []*Field - for _, name := range field.Names { - if name.String() == "Format" { - fmt.Println() - } - fs = append(fs, &Field{ - Anonymous: false, - Name: name.String(), - Type: newType(field.Type), - Comments: newComment(name.String(), field.Comment), - }) - } - return fs - } -} - -type Field struct { - Anonymous bool // 匿名字段 - Name string // 字段名称 - Type *Type // 字段类型 - Comments *Comment // 注释 -} diff --git a/utils/generator/astgo/file.go b/utils/generator/astgo/file.go deleted file mode 100644 index ff4c03c..0000000 --- a/utils/generator/astgo/file.go +++ /dev/null @@ -1,50 +0,0 @@ -package astgo - -import ( - "go/ast" - "go/parser" - "go/token" - "strings" -) - -func newFile(owner *Package, filePath string) (*File, error) { - af, err := parser.ParseFile(token.NewFileSet(), filePath, nil, parser.ParseComments) - if err != nil { - return nil, err - } - f := &File{ - af: af, - owner: owner, - FilePath: filePath, - Comment: newComment("Package", af.Doc), - } - for _, decl := range af.Decls { - switch typ := decl.(type) { - case *ast.FuncDecl: - f.Functions = append(f.Functions, newFunction(typ)) - case *ast.GenDecl: - _, ok := typ.Specs[0].(*ast.TypeSpec) - if ok { - s := newStruct(typ) - f.Structs = append(f.Structs, s) - if strings.HasSuffix(filePath, "_test.go") { - s.Test = true - } - } - } - } - return f, nil -} - -type File struct { - af *ast.File // 抽象语法树文件 - owner *Package // 所属包 - FilePath string // 文件路径 - Structs []*Struct // 包含的结构体 - Functions []*Function // 包含的函数 - Comment *Comment // 文件注释 -} - -func (f *File) Package() string { - return f.af.Name.String() -} diff --git a/utils/generator/astgo/function.go b/utils/generator/astgo/function.go deleted file mode 100644 index 7613476..0000000 --- a/utils/generator/astgo/function.go +++ /dev/null @@ -1,67 +0,0 @@ -package astgo - -import ( - "bytes" - "go/ast" - "go/format" - "go/token" - "strings" -) - -func newFunction(astFunc *ast.FuncDecl) *Function { - f := &Function{ - decl: astFunc, - Name: astFunc.Name.String(), - Comments: newComment(astFunc.Name.String(), astFunc.Doc), - } - f.IsTest = strings.HasPrefix(f.Name, "Test") - f.IsBenchmark = strings.HasPrefix(f.Name, "Benchmark") - f.IsExample = strings.HasPrefix(f.Name, "Example") - f.Test = f.IsTest || f.IsBenchmark || f.IsExample - f.Internal = f.Name[0] >= 97 && f.Name[0] <= 122 - if astFunc.Type.TypeParams != nil { - for _, field := range astFunc.Type.TypeParams.List { - f.Generic = append(f.Generic, newField(field)...) - } - } - if astFunc.Type.Params != nil { - for _, field := range astFunc.Type.Params.List { - f.Params = append(f.Params, newField(field)...) - } - } - if astFunc.Type.Results != nil { - for _, field := range astFunc.Type.Results.List { - f.Results = append(f.Results, newField(field)...) - } - } - if astFunc.Recv != nil { - f.Struct = newField(astFunc.Recv.List[0])[0] - } - return f -} - -type Function struct { - decl *ast.FuncDecl - Name string // 函数名 - Internal bool // 内部函数 - Generic []*Field // 泛型定义 - Params []*Field // 参数字段 - Results []*Field // 返回值字段 - Comments *Comment // 注释 - Struct *Field // 结构体函数对应的结构体字段 - IsExample bool // 是否为测试用例 - IsTest bool // 是否为单元测试 - IsBenchmark bool // 是否为基准测试 - Test bool // 是否为测试函数 -} - -func (f *Function) Code() string { - if f.Test { - f.decl.Doc = nil - } - var bb bytes.Buffer - if err := format.Node(&bb, token.NewFileSet(), f.decl); err != nil { - panic(err) - } - return bb.String() + "\n" -} diff --git a/utils/generator/astgo/name.go b/utils/generator/astgo/name.go deleted file mode 100644 index 89b8d73..0000000 --- a/utils/generator/astgo/name.go +++ /dev/null @@ -1,45 +0,0 @@ -package astgo - -import ( - "go/ast" - "strings" -) - -func newName(expr ast.Expr) string { - var str strings.Builder - switch e := expr.(type) { - //case *ast.KeyValueExpr: - //case *ast.ArrayType: - //case *ast.StructType: - //case *ast.FuncType: - //case *ast.InterfaceType: - //case *ast.MapType: - case *ast.ChanType: - str.WriteString(newName(e.Value)) - case *ast.Ident: - str.WriteString(e.Name) - case *ast.Ellipsis: - str.WriteString(newName(e.Elt)) - //case *ast.BasicLit: - //case *ast.FuncLit: - //case *ast.CompositeLit: - //case *ast.ParenExpr: - case *ast.SelectorExpr: - str.WriteString(newName(e.X)) - case *ast.IndexExpr: - str.WriteString(newName(e.X)) - case *ast.IndexListExpr: - str.WriteString(newName(e.X)) - case *ast.SliceExpr: - str.WriteString(newName(e.X)) - //case *ast.TypeAssertExpr: - //case *ast.CallExpr: - case *ast.StarExpr: - str.WriteString(newName(e.X)) - case *ast.UnaryExpr: - str.WriteString(newName(e.X)) - case *ast.BinaryExpr: - str.WriteString(newName(e.X)) - } - return str.String() -} diff --git a/utils/generator/astgo/package.go b/utils/generator/astgo/package.go deleted file mode 100644 index 1491e94..0000000 --- a/utils/generator/astgo/package.go +++ /dev/null @@ -1,125 +0,0 @@ -package astgo - -import ( - "errors" - "fmt" - "github.com/kercylan98/minotaur/utils/str" - "os" - "path/filepath" -) - -func NewPackage(dir string) (*Package, error) { - pkg := &Package{Dir: dir, Functions: map[string]*Function{}} - fs, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - for _, f := range fs { - path := filepath.Join(pkg.Dir, f.Name()) - if f.IsDir() { - pkg.Dirs = append(pkg.Dirs, path) - continue - } - if filepath.Ext(path) != ".go" { - continue - } - f, err := newFile(pkg, path) - if err != nil { - continue - } - pkg.Files = append(pkg.Files, f) - for _, function := range f.Functions { - function := function - key := function.Name - if function.Struct != nil { - key = fmt.Sprintf("%s.%s", function.Struct.Name, key) - } - pkg.Functions[key] = function - } - } - if len(pkg.Files) == 0 { - return nil, errors.New("not found go ext file") - } - pkg.Name = pkg.Files[0].Package() - - return pkg, nil -} - -type Package struct { - Dir string - Name string - Dirs []string - Files []*File - Functions map[string]*Function -} - -func (p *Package) StructFunc(name string) []*Function { - var fs []*Function - for _, file := range p.Files { - for _, function := range file.Functions { - if function.Struct == nil { - continue - } - if function.Struct.Type.Name == name { - fs = append(fs, function) - } - } - } - return fs -} - -func (p *Package) PackageFunc() []*Function { - var fs []*Function - for _, file := range p.Files { - for _, function := range file.Functions { - if function.Struct == nil { - fs = append(fs, function) - } - } - } - return fs -} - -func (p *Package) Structs() []*Struct { - var structs []*Struct - for _, file := range p.Files { - for _, s := range file.Structs { - structs = append(structs, s) - } - } - return structs -} - -func (p *Package) FileComments() *Comment { - var comment = newComment("", nil) - for _, file := range p.Files { - for _, c := range file.Comment.Comments { - comment.Comments = append(comment.Comments, c) - } - for _, c := range file.Comment.Clear { - comment.Clear = append(comment.Clear, c) - } - } - return comment -} - -func (p *Package) GetUnitTest(f *Function) *Function { - if f.Struct == nil { - return p.Functions[fmt.Sprintf("Test%s", str.FirstUpper(f.Name))] - } - return p.Functions[fmt.Sprintf("Test%s_%s", f.Struct.Type.Name, f.Name)] -} - -func (p *Package) GetExampleTest(f *Function) *Function { - if f.Struct == nil { - return p.Functions[fmt.Sprintf("Example%s", str.FirstUpper(f.Name))] - } - return p.Functions[fmt.Sprintf("Example%s_%s", f.Struct.Type.Name, f.Name)] -} - -func (p *Package) GetBenchmarkTest(f *Function) *Function { - if f.Struct == nil { - return p.Functions[fmt.Sprintf("Benchmark%s", str.FirstUpper(f.Name))] - } - return p.Functions[fmt.Sprintf("Benchmark%s_%s", f.Struct.Type.Name, f.Name)] -} diff --git a/utils/generator/astgo/package_test.go b/utils/generator/astgo/package_test.go deleted file mode 100644 index d775db1..0000000 --- a/utils/generator/astgo/package_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package astgo_test - -import ( - "fmt" - "github.com/kercylan98/minotaur/utils/generator/astgo" - "github.com/kercylan98/minotaur/utils/super" - "testing" -) - -func TestNewPackage(t *testing.T) { - p, err := astgo.NewPackage(`/Users/kercylan/Coding.localized/Go/minotaur/server`) - if err != nil { - panic(err) - } - fmt.Println(string(super.MarshalIndentJSON(p, "", " "))) -} diff --git a/utils/generator/astgo/struct.go b/utils/generator/astgo/struct.go deleted file mode 100644 index 5fba9db..0000000 --- a/utils/generator/astgo/struct.go +++ /dev/null @@ -1,49 +0,0 @@ -package astgo - -import ( - "go/ast" -) - -func newStruct(astGen *ast.GenDecl) *Struct { - astTypeSpec := astGen.Specs[0].(*ast.TypeSpec) - s := &Struct{ - Name: astTypeSpec.Name.String(), - Comments: newComment(astTypeSpec.Name.String(), astGen.Doc), - } - s.Internal = s.Name[0] >= 97 && s.Name[0] <= 122 - if astTypeSpec.TypeParams != nil { - for _, field := range astTypeSpec.TypeParams.List { - s.Generic = append(s.Generic, newField(field)...) - } - } - - switch ts := astTypeSpec.Type.(type) { - case *ast.StructType: - if ts.Fields != nil { - for _, field := range ts.Fields.List { - s.Fields = append(s.Fields, newField(field)...) - } - } - case *ast.InterfaceType: - s.Interface = true - if ts.Methods != nil { - for _, field := range ts.Methods.List { - s.Fields = append(s.Fields, newField(field)...) - } - } - default: - s.Type = newType(ts) - } - return s -} - -type Struct struct { - Name string // 结构体名称 - Internal bool // 内部结构体 - Interface bool // 接口定义 - Comments *Comment // 注释 - Generic []*Field // 泛型类型 - Fields []*Field // 结构体字段 - Type *Type // 和结构体字段二选一,处理类似 type a string 的情况 - Test bool // 是否是测试结构体 -} diff --git a/utils/generator/astgo/type.go b/utils/generator/astgo/type.go deleted file mode 100644 index 4e4baf9..0000000 --- a/utils/generator/astgo/type.go +++ /dev/null @@ -1,139 +0,0 @@ -package astgo - -import ( - "fmt" - "go/ast" - "strings" -) - -func newType(expr ast.Expr) *Type { - var typ = &Type{ - expr: expr, - Name: newName(expr), - } - var str strings.Builder - switch e := typ.expr.(type) { - case *ast.KeyValueExpr: - k, v := newType(e.Key), newType(e.Value) - str.WriteString(fmt.Sprintf("%s : %s", k.Sign, v.Sign)) - case *ast.ArrayType: - isSlice := e.Len == nil - str.WriteByte('[') - if !isSlice { - length := newType(e.Len) - str.WriteString(length.Sign) - } - str.WriteByte(']') - element := newType(e.Elt) - str.WriteString(element.Sign) - case *ast.StructType: - str.WriteString("struct {") - if e.Fields != nil && len(e.Fields.List) > 0 { - str.WriteString("\n") - for _, field := range e.Fields.List { - f := newField(field)[0] - str.WriteString(fmt.Sprintf("%s %s\n", f.Name, f.Type.Sign)) - } - } - str.WriteString("}") - case *ast.FuncType: - var handler = func(fls *ast.FieldList) string { - var s string - if fls != nil { - var brackets bool - var params []string - for _, field := range fls.List { - f := newField(field)[0] - if !f.Anonymous { - brackets = true - } - params = append(params, fmt.Sprintf("%s %s", f.Name, f.Type.Sign)) - } - s = strings.Join(params, ", ") - if brackets && strings.HasSuffix(s, ")") && strings.HasPrefix(s, "(") { - s = "(" + s + ")" - } - } - return s - } - str.WriteString(strings.TrimSpace(fmt.Sprintf("func %s %s", func() string { - f := handler(e.Params) - if len(strings.TrimSpace(f)) == 0 { - return "()" - } - if !strings.HasPrefix(f, "(") { - return "(" + f + ")" - } - return f - }(), func() string { - f := handler(e.Results) - if e.Results != nil && len(e.Results.List) >= 2 && !strings.HasSuffix(f, "(") { - return "(" + f + ")" - } - return f - }()))) - case *ast.InterfaceType: - str.WriteString("interface {") - if e.Methods != nil && len(e.Methods.List) > 0 { - str.WriteString("\n") - for _, field := range e.Methods.List { - f := newField(field)[0] - str.WriteString(fmt.Sprintf("%s\n", f.Type.Sign)) - } - } - str.WriteString("}") - case *ast.MapType: - k, v := newType(e.Key), newType(e.Value) - str.WriteString(fmt.Sprintf("map[%s]%s", k.Sign, v.Sign)) - case *ast.ChanType: - t := newType(e.Value) - str.WriteString(fmt.Sprintf("chan %s", t.Sign)) - case *ast.Ident: - str.WriteString(e.Name) - case *ast.Ellipsis: - element := newType(e.Elt) - str.WriteString(fmt.Sprintf("...%s", element.Sign)) - case *ast.BasicLit: - str.WriteString(e.Value) - //case *ast.FuncLit: - //case *ast.CompositeLit: - //case *ast.ParenExpr: - case *ast.SelectorExpr: - t := newType(e.X) - str.WriteString(fmt.Sprintf("%s.%s", t.Sign, e.Sel.Name)) - case *ast.IndexExpr: - t := newType(e.X) - generic := newType(e.Index) - str.WriteString(fmt.Sprintf("%s[%s]", t.Sign, generic.Sign)) - case *ast.IndexListExpr: - self := newType(e.X) - var genericStr []string - for _, index := range e.Indices { - g := newType(index) - genericStr = append(genericStr, g.Sign) - } - str.WriteString(fmt.Sprintf("%s[%s]", self.Sign, strings.Join(genericStr, ", "))) - case *ast.SliceExpr: - case *ast.TypeAssertExpr: - case *ast.CallExpr: - case *ast.StarExpr: - typ.IsPointer = true - t := newType(e.X) - str.WriteString(fmt.Sprintf("*%s", t.Sign)) - case *ast.UnaryExpr: - str.WriteString(fmt.Sprintf("%s%s", e.Op.String(), newType(e.X).Sign)) - case *ast.BinaryExpr: - str.WriteString(newType(e.X).Sign) - str.WriteString(fmt.Sprintf(" %s ", e.Op.String())) - str.WriteString(newType(e.Y).Sign) - } - typ.Sign = str.String() - return typ -} - -type Type struct { - expr ast.Expr - Sign string // 类型签名 - IsPointer bool // 指针类型 - Name string // 类型名称 -} diff --git a/utils/generator/genreadme/README.md b/utils/generator/genreadme/README.md deleted file mode 100644 index cbfe512..0000000 --- a/utils/generator/genreadme/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Genreadme - -[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur) -![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat) - -暂无介绍... - - -## 目录导航 -列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️ -
-展开 / 折叠目录导航 - - -> 包级函数定义 - -|函数名称|描述 -|:--|:-- -|[New](#New)|暂无描述... - - -> 类型定义 - -|类型|名称|描述 -|:--|:--|:-- -|`STRUCT`|[Builder](#struct_Builder)|暂无描述... - -
- - -*** -## 详情信息 -#### func New(pkgDirPath string, output string) (*Builder, error) - - -*** - -### Builder `STRUCT` - -```go -type Builder struct { - p *astgo.Package - b *strings.Builder - o string -} -``` - - -#### func (*Builder) Generate() error - -
-查看 / 收起单元测试 - - -```go - -func TestBuilder_Generate(t *testing.T) { - filepath.Walk("D:/sources/minotaur", func(path string, info fs.FileInfo, err error) error { - if !info.IsDir() { - return nil - } - if strings.Contains(strings.TrimPrefix(path, "D:/sources/minotaur"), ".") { - return nil - } - b, err := New(path, filepath.Join(path, "README.md")) - if err != nil { - return nil - } - if err = b.Generate(); err != nil { - panic(err) - } - return nil - }) -} - -``` - - -
- - -*** diff --git a/utils/generator/genreadme/builder.go b/utils/generator/genreadme/builder.go deleted file mode 100644 index a15f662..0000000 --- a/utils/generator/genreadme/builder.go +++ /dev/null @@ -1,408 +0,0 @@ -package genreadme - -import ( - "fmt" - "github.com/kercylan98/minotaur/utils/collection" - "github.com/kercylan98/minotaur/utils/file" - "github.com/kercylan98/minotaur/utils/generator/astgo" - "github.com/kercylan98/minotaur/utils/str" - "github.com/kercylan98/minotaur/utils/super" - "go/format" - "strings" - "sync" -) - -func New(pkgDirPath string, output string) (*Builder, error) { - p, err := astgo.NewPackage(pkgDirPath) - if err != nil { - return nil, err - } - b := &Builder{ - p: p, - b: new(strings.Builder), - o: output, - } - return b, nil -} - -type Builder struct { - p *astgo.Package - b *strings.Builder - o string -} - -func (b *Builder) Generate() error { - //fmt.Println(string(super.MarshalIndentJSON(b.p, "", " "))) - b.genHeader() - b.genMenus() - b.genStructs() - - return file.WriterFile(b.o, []byte(b.b.String())) -} - -func (b *Builder) genHeader() { - b.title(1, str.FirstUpper(b.p.Name)) - b.newLine() - b.newLine(fmt.Sprintf(`[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)`)) - b.newLine(fmt.Sprintf(`![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)`)) - if len(b.p.FileComments().Clear) != 0 { - b.newLine().newLine(b.p.FileComments().Clear...).newLine() - } else { - b.newLine().newLine("暂无介绍...").newLine() - } - b.newLine() -} - -func (b *Builder) genMenus() { - var genTitleOnce sync.Once - var genTitle = func() { - genTitleOnce.Do(func() { - b.title(2, "目录导航") - b.newLine("列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️") - b.detailsStart("展开 / 折叠目录导航") - }) - } - - packageFunction := b.p.PackageFunc() - var structList []*astgo.Struct - for _, f := range b.p.Files { - if strings.HasSuffix(f.FilePath, "_test.go") { - continue - } - for _, structInfo := range f.Structs { - if structInfo.Test || structInfo.Internal { - continue - } - structList = append(structList, structInfo) - } - } - - if len(packageFunction) > 0 { - var pfGenOnce sync.Once - var pfGen = func() { - pfGenOnce.Do(func() { - genTitle() - b.quote("包级函数定义").newLine() - b.tableCel("函数名称", "描述") - }) - } - for _, function := range packageFunction { - if function.Test || function.Internal { - continue - } - pfGen() - b.tableRow( - fmt.Sprintf("[%s](#%s)", function.Name, function.Name), - collection.FindFirstOrDefaultInSlice(function.Comments.Clear, "暂无描述..."), - ) - } - b.newLine().newLine() - } - - if len(structList) > 0 { - var structGenOnce sync.Once - var structGen = func() { - structGenOnce.Do(func() { - genTitle() - b.quote("类型定义").newLine() - b.tableCel("类型", "名称", "描述") - }) - } - for _, structInfo := range structList { - if structInfo.Test || structInfo.Internal { - continue - } - structGen() - b.tableRow( - super.If(structInfo.Interface, "`INTERFACE`", "`STRUCT`"), - fmt.Sprintf("[%s](#struct_%s)", structInfo.Name, structInfo.Name), - collection.FindFirstOrDefaultInSlice(structInfo.Comments.Clear, "暂无描述..."), - ) - } - } - b.detailsEnd() - b.newLine("***") -} - -func (b *Builder) genStructs() { - var titleOnce sync.Once - var titleBuild = func() { - titleOnce.Do(func() { - b.title(2, "详情信息") - }) - } - - var funcHandler = func(params []*astgo.Field) string { - var s string - var brackets bool - var paramsStr []string - for _, field := range params { - if !field.Anonymous { - brackets = true - } - paramsStr = append(paramsStr, fmt.Sprintf("%s %s", field.Name, field.Type.Sign)) - } - s = strings.Join(paramsStr, ", ") - if brackets { - s = "(" + s + ")" - } - return s - } - - for _, function := range b.p.PackageFunc() { - if function.Internal || function.Test { - continue - } - titleBuild() - b.title(4, strings.TrimSpace(fmt.Sprintf("func %s%s%s %s", - function.Name, - func() string { - var sb strings.Builder - if len(function.Generic) > 0 { - sb.WriteString("\\[") - var genericStr = make([]string, 0) - for _, field := range function.Generic { - genericStr = append(genericStr, fmt.Sprintf("%s %s", field.Name, field.Type.Sign)) - } - sb.WriteString(strings.Join(genericStr, ", ")) - sb.WriteString("\\]") - } - return sb.String() - }(), - func() string { - f := funcHandler(function.Params) - if !strings.HasPrefix(f, "(") { - f = "(" + f + ")" - } - return f - }(), - func() string { - f := strings.TrimSpace(funcHandler(function.Results)) - if len(function.Results) >= 2 && !strings.HasPrefix(f, "(") { - f = "(" + f + ")" - } - return f - }(), - ))) - b.newLine(fmt.Sprintf(``, function.Name)) - b.quote() - for _, comment := range function.Comments.Clear { - b.quote(comment) - } - b.newLine() - if example := b.p.GetExampleTest(function); example != nil { - b.newLine("**示例代码:**").newLine() - if len(example.Comments.Clear) > 0 { - for _, s := range example.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", example.Code(), "```\n") - } - if unitTest := b.p.GetUnitTest(function); unitTest != nil { - b.detailsStart("查看 / 收起单元测试") - if len(unitTest.Comments.Clear) > 0 { - for _, s := range unitTest.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", unitTest.Code(), "```\n") - b.detailsEnd() - } - if benchmarkTest := b.p.GetBenchmarkTest(function); benchmarkTest != nil { - b.detailsStart("查看 / 收起基准测试") - if len(benchmarkTest.Comments.Clear) > 0 { - for _, s := range benchmarkTest.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", benchmarkTest.Code(), "```\n") - b.detailsEnd() - } - b.newLine("***") - } - - for _, f := range b.p.Files { - for _, structInfo := range f.Structs { - if structInfo.Internal || structInfo.Test { - continue - } - titleBuild() - b.newLine(fmt.Sprintf(``, structInfo.Name)) - b.title(3, fmt.Sprintf("%s `%s`", structInfo.Name, super.If(structInfo.Interface, "INTERFACE", "STRUCT"))) - b.newLine(structInfo.Comments.Clear...) - b.newLine("```go") - structDefine := fmt.Sprintf("type %s %s", - func() string { - var sb strings.Builder - sb.WriteString(structInfo.Name) - if len(structInfo.Generic) > 0 { - sb.WriteString("[") - var gs []string - for _, field := range structInfo.Generic { - gs = append(gs, fmt.Sprintf("%s %s", field.Name, field.Type.Sign)) - } - sb.WriteString(strings.Join(gs, ", ")) - sb.WriteString("]") - } - return sb.String() - }(), - func() string { - var sb strings.Builder - if structInfo.Type == nil { - var head = "struct" - if structInfo.Interface { - head = "interface" - } - sb.WriteString(head + " {") - if len(structInfo.Fields) > 0 { - sb.WriteString("\n") - } - if structInfo.Interface { - for _, field := range structInfo.Fields { - sb.WriteString(fmt.Sprintf("\t%s %s\n", field.Name, strings.TrimPrefix(field.Type.Sign, "func"))) - } - } else { - for _, field := range structInfo.Fields { - sb.WriteString(fmt.Sprintf("\t%s %s\n", field.Name, field.Type.Sign)) - } - } - sb.WriteString("}") - } else { - sb.WriteString(structInfo.Type.Sign) - } - return sb.String() - }()) - sdb, err := format.Source([]byte(structDefine)) - if err != nil { - fmt.Println(structDefine) - panic(err) - } - b.newLine(string(sdb)) - b.newLine("```") - - for _, function := range b.p.StructFunc(structInfo.Name) { - if function.Internal || function.Test { - continue - } - b.newLine(fmt.Sprintf(``, structInfo.Name, function.Name)).newLine() - b.title(4, strings.TrimSpace(fmt.Sprintf("func (%s%s) %s%s %s", - super.If(function.Struct.Type.IsPointer, "*", ""), - structInfo.Name, - function.Name, - func() string { - f := funcHandler(function.Params) - if !strings.HasPrefix(f, "(") { - f = "(" + f + ")" - } - return f - }(), - func() string { - f := funcHandler(function.Results) - if len(function.Results) >= 2 && !strings.HasPrefix(strings.TrimSpace(f), "(") { - f = "(" + f + ")" - } - return f - }(), - ))) - b.quote() - for _, comment := range function.Comments.Clear { - b.quote(comment) - } - b.newLine() - if example := b.p.GetExampleTest(function); example != nil { - b.newLine("**示例代码:**").newLine() - if len(example.Comments.Clear) > 0 { - for _, s := range example.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", example.Code(), "```\n") - } - if unitTest := b.p.GetUnitTest(function); unitTest != nil { - b.detailsStart("查看 / 收起单元测试") - if len(unitTest.Comments.Clear) > 0 { - for _, s := range unitTest.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", unitTest.Code(), "```\n") - b.detailsEnd() - } - if benchmarkTest := b.p.GetBenchmarkTest(function); benchmarkTest != nil { - b.detailsStart("查看 / 收起基准测试") - if len(benchmarkTest.Comments.Clear) > 0 { - for _, s := range benchmarkTest.Comments.Clear { - b.newLine(fmt.Sprintf("%s", s)) - } - b.newLine().newLine() - } - b.newLine("```go\n", benchmarkTest.Code(), "```\n") - b.detailsEnd() - } - b.newLine("***") - } - } - } -} - -func (b *Builder) newLine(text ...string) *Builder { - if len(text) == 0 { - b.b.WriteString("\n") - return b - } - for _, s := range text { - b.b.WriteString(s + "\n") - } - return b -} - -func (b *Builder) title(lv int, str string) *Builder { - var l string - for i := 0; i < lv; i++ { - l += "#" - } - b.b.WriteString(l + " " + str) - b.b.WriteString("\n") - return b -} - -func (b *Builder) quote(str ...string) *Builder { - for _, s := range str { - b.b.WriteString("> " + s) - b.newLine() - } - return b -} - -func (b *Builder) tableCel(str ...string) *Builder { - var a string - var c string - for _, s := range str { - a += "|" + s - c += "|:--" - } - b.newLine(a, c) - return b -} - -func (b *Builder) tableRow(str ...string) *Builder { - var c string - for _, s := range str { - c += "|" + s - } - return b.newLine(c) -} - -func (b *Builder) detailsStart(title string) *Builder { - return b.newLine("
", ""+title+"").newLine().newLine() -} - -func (b *Builder) detailsEnd() *Builder { - return b.newLine().newLine("
").newLine().newLine() -} diff --git a/utils/generator/genreadme/builder_test.go b/utils/generator/genreadme/builder_test.go deleted file mode 100644 index ded811b..0000000 --- a/utils/generator/genreadme/builder_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package genreadme - -import ( - "io/fs" - "path/filepath" - "strings" - "testing" -) - -func TestBuilder_Generate(t *testing.T) { - //b, err := New(`/Users/kercylan/Coding.localized/Go/minotaur/utils/buffer`, `/Users/kercylan/Coding.localized/Go/minotaur/utils/buffer/README.md`) - //if err != nil { - // panic(err) - //} - //if err = b.Generate(); err != nil { - // panic(err) - //} - //return - filepath.Walk("D:/sources/minotaur", func(path string, info fs.FileInfo, err error) error { - if !info.IsDir() { - return nil - } - if strings.Contains(strings.TrimPrefix(path, "D:/sources/minotaur"), ".") { - return nil - } - b, err := New( - path, - filepath.Join(path, "README.md"), - ) - if err != nil { - return nil - } - if err = b.Generate(); err != nil { - panic(err) - } - return nil - }) - -}