/* * Copyright 2025 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package tools import ( "context" "fmt" "sort" "time" ) type ActivityType string const ( ActivityTypeAttraction ActivityType = "attraction" ActivityTypePerformance ActivityType = "performance" ActivityTypeRestaurant ActivityType = "restaurant" ActivityTypeOther ActivityType = "other" ) // Activity 主题乐园中的一个项目,可以是游乐设施、表演或餐厅. type Activity struct { Name string `json:"name"` Desc string `json:"desc"` Type ActivityType `json:"type"` Location string `json:"location" jsonschema:"description=项目所属的区域"` MinHeight int `json:"min_height,omitempty" jsonschema:"description=参加游乐设施需要的最小身高,单位是厘米。如果为空,则没有身高要求"` Duration int `json:"duration,omitempty" jsonschema:"description=一个项目参加一次需要的时间,注意不包括排队的时间。如果为空,则缺少具体的时间信息,可以默认为 10 分钟"` TimeTable []string `json:"time_table,omitempty" jsonschema:"description=一个演出的时间表。如果为空,则使用 OpenTime 和 CloseTime 来表示这个项目的运营时间范围"` OpenTime string `json:"open_time,omitempty" jsonschema:"description=一个项目开始运营的时间"` CloseTime string `json:"close_time,omitempty" jsonschema:"description=一个项目结束运营的时间"` RequireBooking bool `json:"require_booking,omitempty" jsonschema:"description=一个餐厅是否需要提前预约"` HasPriorityAccess bool `json:"has_priority_access,omitempty" jsonschema:"description=一个项目是否有高速票服务"` PriorityAccessCost int `json:"priority_access_cost,omitempty" jsonschema:"description=一个项目如果有高速票服务,则一个人的高速票需要花多少钱"` QueueTime int `json:"queue_time,omitempty" jsonschema:"description=一个项目常规需要的排队时间,单位是分钟。如果为空,则这个项目一般不需要排队"` } // LocationAdjacency 主题乐园的一个区域到相邻区域的步行时间. type LocationAdjacency struct { FromLocationName string `json:"from_location_name" jsonschema:"description=从哪个区域开始计算距离"` DestinationLocationWalkingTimes []DestinationWalkingTime `json:"destination_location_walking_times,omitempty" jsonschema:"description=相邻区域列表,包含走路的分钟数"` } type DestinationWalkingTime struct { DestinationName string `json:"destination_name" jsonschema:"description=目标区域名称"` WalkTime int `json:"walk_time" jsonschema:"description=步行到目标区域所需要的分钟数"` } // AttractionQueueTime 主题乐园的一个游乐项目的排队时间. type AttractionQueueTime struct { Name string `json:"name" jsonschema:"description=游乐项目的名称"` QueueTime int `json:"queue_time" jsonschema:"description=游乐项目的排队时间"` } type ListPerformanceRequest struct { Name string `json:"name,omitempty" jsonschema:"description=演出的名称,如果不需要查询具体的某个演出,此处传空"` Location string `json:"location,omitempty" jsonschema:"description=演出所在的区域,如果不需要查询具体某个区域的演出,此处传空"` } type ListPerformanceResponse struct { Performances []Activity `json:"performances" jsonschema:"description=符合查询条件的所有演出的信息"` } type ListAttractionRequest struct { Name string `json:"name,omitempty" jsonschema:"description=游乐项目的名称,如果不需要查询具体的某个游乐项目,此处传空"` Location string `json:"location,omitempty" jsonschema:"description=游乐项目所在的区域,如果不需要查询具体某个区域的游乐项目,此处传空"` } type ListAttractionResponse struct { Attractions []Activity `json:"attractions" jsonschema:"description=符合查询条件的所有游乐项目的信息"` } type ListRestaurantRequest struct { Name string `json:"name,omitempty" jsonschema:"description=餐厅的名称,如果不需要查询具体的某个餐厅,此处传空"` Location string `json:"location,omitempty" jsonschema:"description=餐厅所在的区域,如果不需要查询具体某个区域的餐厅,此处传空"` } type ListRestaurantResponse struct { Restaurants []Activity `json:"restaurants" jsonschema:"description=符合查询条件的所有餐厅的信息"` } type ListAttractionQueueTimeRequest struct { Name string `json:"name,omitempty" jsonschema:"description=游乐项目的名称,如果不需要查询具体的某个游乐项目的排队时间,此处传空"` Location string `json:"location,omitempty" jsonschema:"description=游乐项目所在的区域,如果不需要查询具体某个区域的游乐项目的排队时间,此处传空"` } type ListAttractionQueueTimeResponse struct { QueueTime []AttractionQueueTime `json:"queue_time" jsonschema:"description=符合查询条件的所有游乐项目的排队时间"` } type ListAdjacentLocationRequest struct{} type ListAdjacentLocationResponse struct { AdjacencyList []LocationAdjacency `json:"adjacency_list" jsonschema:"description=所有区域的邻接区域"` } type GetParkHourRequest struct{} type GetParkHourResponse struct { OpenHour string `json:"open_hour"` CloseHour string `json:"close_hour"` } type GetParkTicketPriceRequest struct{} type GetParkTicketPriceResponse struct { Price string `json:"price" jsonschema:"description=乐园门票价格信息"` } type ListLocationsRequest struct{} type ListLocationsResponse struct { Locations []string `json:"locations"` } func ListLocations(_ context.Context, _ *ListLocationsRequest) (out *ListLocationsResponse, err error) { return &ListLocationsResponse{ Locations: []string{ "幻想世界", "未来世界", "冒险岛", "宝贝港湾", "入口大街", "奇幻园林", "热情小动物城市", "玩具的故事", }, }, nil } type QueryEntranceRequest struct{} type QueryEntranceResponse struct { EntranceLocation string `json:"entrance_location" jsonschema:"description=园区入口区域名称"` } func QueryEntrance(_ context.Context, _ *QueryEntranceRequest) (out *QueryEntranceResponse, err error) { return &QueryEntranceResponse{EntranceLocation: "入口大街"}, nil } // GetAdjacentLocation 获取相邻的地点,可获取全部地点到其邻接地点的步行分钟数,也可获取单个地点到其邻接地点的步行分钟数. func GetAdjacentLocation(_ context.Context, _ *ListAdjacentLocationRequest) (out *ListAdjacentLocationResponse, err error) { adjacencyMap := make(map[string][]DestinationWalkingTime) for k, v := range locationAdjacencyMap { if _, ok := adjacencyMap[k]; !ok { adjacencyMap[k] = make([]DestinationWalkingTime, 0) } for dest, walkingTime := range v { adjacencyMap[k] = append(adjacencyMap[k], DestinationWalkingTime{ DestinationName: dest, WalkTime: walkingTime, }) } } adjacencyList := make([]LocationAdjacency, 0) for k, v := range adjacencyMap { adjacencyList = append(adjacencyList, LocationAdjacency{ FromLocationName: k, DestinationLocationWalkingTimes: v, }) } return &ListAdjacentLocationResponse{ AdjacencyList: adjacencyList, }, nil } func GetParkTicketPrice(_ context.Context, _ *GetParkTicketPriceRequest) (out *GetParkTicketPriceResponse, err error) { return &GetParkTicketPriceResponse{Price: "成人票 400,儿童票 300"}, nil } // GetParkHour 获取乐园的营业时间. func GetParkHour(_ context.Context, _ *GetParkHourRequest) (out *GetParkHourResponse, err error) { return &GetParkHourResponse{ OpenHour: "09:00", CloseHour: "21:30", }, nil } // GetQueueTime 获取游乐设施排队时间. func GetQueueTime(_ context.Context, in *ListAttractionQueueTimeRequest) (out *ListAttractionQueueTimeResponse, err error) { if len(in.Name) > 0 { for _, a := range attractions { if a.Name == in.Name { return &ListAttractionQueueTimeResponse{ QueueTime: []AttractionQueueTime{ { Name: a.Name, QueueTime: a.QueueTime, }, }, }, nil } } } if len(in.Location) > 0 { queueTimes := make([]AttractionQueueTime, 0) for _, a := range attractions { if a.Location == in.Location { queueTimes = append(queueTimes, AttractionQueueTime{ Name: a.Name, QueueTime: a.QueueTime, }) return &ListAttractionQueueTimeResponse{ QueueTime: queueTimes, }, nil } } } queueTimes := make([]AttractionQueueTime, 0) for _, a := range attractions { queueTimes = append(queueTimes, AttractionQueueTime{ Name: a.Name, QueueTime: a.QueueTime, }) } return &ListAttractionQueueTimeResponse{ QueueTime: queueTimes, }, nil } // GetAttractionInfo 获取游乐设施信息. func GetAttractionInfo(_ context.Context, in *ListAttractionRequest) (out *ListAttractionResponse, err error) { if len(in.Name) > 0 && in.Name != "all" { for _, a := range attractions { if a.Name == in.Name { return &ListAttractionResponse{ Attractions: []Activity{ a, }, }, nil } } } if len(in.Location) > 0 { locationAttractions := make([]Activity, 0) for _, a := range attractions { if a.Location == in.Location { locationAttractions = append(locationAttractions, a) return &ListAttractionResponse{ Attractions: locationAttractions, }, nil } } } return &ListAttractionResponse{ Attractions: attractions, }, nil } // GetPerformanceInfo 获取演出信息. func GetPerformanceInfo(_ context.Context, in *ListPerformanceRequest) (out *ListPerformanceResponse, err error) { if len(in.Name) > 0 && in.Name != "all" { for _, a := range performances { if a.Name == in.Name { return &ListPerformanceResponse{ Performances: []Activity{ a, }, }, nil } } } if len(in.Location) > 0 { locationPerformances := make([]Activity, 0) for _, a := range performances { if a.Location == in.Location { locationPerformances = append(locationPerformances, a) return &ListPerformanceResponse{ Performances: locationPerformances, }, nil } } } return &ListPerformanceResponse{ Performances: performances, }, nil } // GetRestaurantInfo 获取餐厅信息. func GetRestaurantInfo(_ context.Context, in *ListRestaurantRequest) (out *ListRestaurantResponse, err error) { if len(in.Name) > 0 && in.Name != "all" { for _, a := range restaurants { if a.Name == in.Name { return &ListRestaurantResponse{ Restaurants: []Activity{ a, }, }, nil } } } if len(in.Location) > 0 { locationRestaurants := make([]Activity, 0) for _, a := range restaurants { if a.Location == in.Location { locationRestaurants = append(locationRestaurants, a) return &ListRestaurantResponse{ Restaurants: locationRestaurants, }, nil } } } return &ListRestaurantResponse{ Restaurants: restaurants, }, nil } type OnePerformanceStartTime struct { PerformanceName string `json:"performance_name"` StartTime string `json:"start_time" jsonschema:"description=选中的演出开始时间,格式如15:30"` } type ValidatePerformanceTimeTableRequest struct { PerformancesStartTime []OnePerformanceStartTime `json:"performances_start_time" jsonschema:"description=用户选择的演出名称和开始时间"` } type PerformanceStartTimeValidateResult struct { PerformanceName string `json:"performance_name"` StartTime string `json:"start_time"` IsValid bool `json:"is_valid"` ErrMessage string `json:"err_message"` } type ValidatePerformanceTimeTableResponse struct { PerformancesValidateResult []PerformanceStartTimeValidateResult `json:"performances_validate_result" jsonschema:"description=验证结果,只包含有问题的表演"` } func ValidatePerformanceTimeTable(_ context.Context, in *ValidatePerformanceTimeTableRequest) (out *ValidatePerformanceTimeTableResponse, err error) { results := make([]PerformanceStartTimeValidateResult, 0, len(in.PerformancesStartTime)) for _, performance := range in.PerformancesStartTime { var ( performanceFound bool timeTable []string ) for _, p := range getPerformances() { if p.Name == performance.PerformanceName { for _, t := range p.TimeTable { if t == performance.StartTime { break } } performanceFound = true timeTable = p.TimeTable break } } if !performanceFound { results = append(results, PerformanceStartTimeValidateResult{ PerformanceName: performance.PerformanceName, StartTime: performance.StartTime, IsValid: false, ErrMessage: fmt.Sprintf("传入的表演名称为 %s,未找到对应的表演", performance.PerformanceName), }) } else if !contains(timeTable, performance.StartTime) { results = append(results, PerformanceStartTimeValidateResult{ PerformanceName: performance.PerformanceName, StartTime: performance.StartTime, IsValid: false, ErrMessage: fmt.Sprintf("传入的表演名称为 %s,传入的开始时间为 %s,但该表演实际的时间表为 %v", performance.PerformanceName, performance.StartTime, timeTable), }) } } return &ValidatePerformanceTimeTableResponse{ PerformancesValidateResult: results, }, nil } // contains is a helper function to check if a slice contains a string func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } type ArrangePerformancesRequest struct { ChosenPerformances []string `json:"chosen_performances" jsonschema:"description=用户选择的演出名称列表"` } type ArrangePerformancesResponse struct { ArrangedPerformances []PerformanceTime `json:"arranged_performances" jsonschema:"description=根据用户选择的演出,以及实际的时间表和演出时长,计算出的看演出的时间规划。包含了每场演出的排队、占座时间。"` UnsatisfiedPerformances []string `json:"unsatisfied_performances" jsonschema:"description=由于跟其他演出的时间冲突,无法完成时间安排的演出名称"` } type PerformanceTime struct { PerformanceName string `json:"performance_name" jsonschema:"description=演出名称"` StartTime string `json:"start_time" jsonschema:"description=演出开始时间"` EndTime string `json:"end_time" jsonschema:"description=演出结束时间"` } func getPerformances() []Activity { return performances } func ArrangePerformances(_ context.Context, in *ArrangePerformancesRequest) (out *ArrangePerformancesResponse, err error) { performanceInfos := make(map[string]Activity) for _, p := range getPerformances() { performanceInfos[p.Name] = p } chosenPerformanceInfos := make(map[string]Activity, len(in.ChosenPerformances)) for _, chosen := range in.ChosenPerformances { if _, ok := performanceInfos[chosen]; !ok { return nil, fmt.Errorf("传入的演出名称为 %s,未找到对应的表演", chosen) } chosenPerformanceInfos[chosen] = performanceInfos[chosen] } arranged := make(map[string]PerformanceTime) unsatisfied := make(map[string]bool) // Define a helper function to parse time and calculate end time parseTimeAndEnd := func(startTimeStr string, duration int) (time.Time, time.Time, error) { startTime, err := time.Parse("15:04", startTimeStr) if err != nil { return time.Time{}, time.Time{}, err } endTime := startTime.Add(time.Duration(duration) * time.Minute) return startTime, endTime, nil } // Define a helper function to check if there is a time conflict hasConflict := func(startTime, endTime time.Time) bool { twentyMinutes := 20 * time.Minute for _, perf := range arranged { existingStart, err := time.Parse("15:04", perf.StartTime) if err != nil { continue } existingEnd, err := time.Parse("15:04", perf.EndTime) if err != nil { continue } // 检查时间冲突,包括间隔小于 20 分钟的情况 if (startTime.Before(existingEnd.Add(twentyMinutes)) && endTime.After(existingStart.Add(-twentyMinutes))) || startTime.Equal(existingEnd.Add(twentyMinutes)) || endTime.Equal(existingStart.Add(-twentyMinutes)) { return true } } return false } // Sort chosen performances by the number of available time slots in ascending order sortedChosenPerformances := make([]string, 0, len(chosenPerformanceInfos)) for name := range chosenPerformanceInfos { sortedChosenPerformances = append(sortedChosenPerformances, name) } sort.Slice(sortedChosenPerformances, func(i, j int) bool { return len(chosenPerformanceInfos[sortedChosenPerformances[i]].TimeTable) < len(chosenPerformanceInfos[sortedChosenPerformances[j]].TimeTable) }) for _, name := range sortedChosenPerformances { info := chosenPerformanceInfos[name] var foundValidTime bool for _, t := range info.TimeTable { startTime, endTime, err := parseTimeAndEnd(t, info.Duration) if err != nil { continue } if !hasConflict(startTime, endTime) { arranged[name] = PerformanceTime{ PerformanceName: name, StartTime: startTime.Format("15:04"), EndTime: endTime.Format("15:04"), } foundValidTime = true break } } if !foundValidTime { unsatisfied[name] = true } } sorted := make([]PerformanceTime, 0, len(arranged)) for k := range arranged { sorted = append(sorted, arranged[k]) } sort.Slice(sorted, func(i, j int) bool { return sorted[i].EndTime < sorted[j].EndTime }) unsatisfiedPerformances := make([]string, 0, len(unsatisfied)) for k := range unsatisfied { unsatisfiedPerformances = append(unsatisfiedPerformances, k) } return &ArrangePerformancesResponse{ ArrangedPerformances: sorted, UnsatisfiedPerformances: unsatisfiedPerformances, }, nil } type PlanItem struct { ActivityType ActivityType `json:"activity_type" jsonschema:"description=活动类型,enum:attraction,enum:performance,enum:restaurant,enum:other"` StartTime string `json:"start_time" jsonschema:"description=活动开始时间,格式为15:04"` PerformanceStartTime *string `json:"performance_start_time" jsonschema:"description=如果为 performance 类型,是演出开始时间,格式为15:04. 否则忽略这个参数或传 NULL"` Duration *int `json:"duration" jsonschema:"description=如果为 performance 类型,是演出时长,如果为 attraction 类型,是实际玩的时长,单位为分钟,否则忽略这个参数或传 Null"` QueueTime *int `json:"queue_time" jsonschema:"description=排队时间,单位为分钟,attraction 类型必填,否则忽略这个参数或传 NULL。如果用了高速票,QueueTime使用0"` Location string `json:"location" jsonschema:"description=计划所在的区域,如果是 move 类型的计划,这里是目标区域。可以填写的区域列表为通过 list_locations 获取的清单"` ActivityName string `json:"activity_name,omitempty" jsonschema:"description=move 之外的其他类型的计划,这里是项目的准确名称,如 attraction,performance,restaurant 的准确名称"` } type PlanItemValidationResult struct { PlanItem PlanItem `json:"plan_item" jsonschema:"description=完整一日计划其中的一个计划项"` IsValid bool `json:"is_valid"` ErrMsg string `json:"err_msg"` } type ValidatePlanItemsRequest struct { PlanItems []PlanItem `json:"plan_items" jsonschema:"description=一个完整的一日日程计划,包含了所有的活动和移动计划,按时间先后顺序排列"` } type ValidatePlanItemsResponse struct { ValidationResults []PlanItemValidationResult `json:"validation_results" jsonschema:"description=每个计划项的验证结果,包含是否有效和错误信息,只包含有问题的计划项"` } func ValidatePlanItems(_ context.Context, in *ValidatePlanItemsRequest) (out *ValidatePlanItemsResponse, err error) { results := make([]PlanItemValidationResult, 0, len(in.PlanItems)) for i, item := range in.PlanItems { result := PlanItemValidationResult{ PlanItem: item, IsValid: true, ErrMsg: "", } switch item.ActivityType { case "表演", ActivityTypePerformance: item.ActivityType = ActivityTypePerformance case "游乐设施", ActivityTypeAttraction: item.ActivityType = ActivityTypeAttraction case "餐厅", ActivityTypeRestaurant: item.ActivityType = ActivityTypeRestaurant default: item.ActivityType = ActivityTypeOther } // 1. validate basic information is correct by query activity name, location, activityType, performance duration if item.ActivityType != ActivityTypeAttraction && item.ActivityType != ActivityTypePerformance && item.ActivityType != ActivityTypeRestaurant && item.ActivityType != ActivityTypeOther { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid activity type %s for plan item %s", item.ActivityType, item.ActivityName) } if item.ActivityType == ActivityTypeAttraction && (item.QueueTime == nil || *item.QueueTime < 0) { result.IsValid = false result.ErrMsg = fmt.Sprintf("Queue time must be non-negative for attraction %s", item.ActivityName) } if item.ActivityType == ActivityTypePerformance { if item.Duration == nil || *item.Duration <= 0 { result.IsValid = false result.ErrMsg = fmt.Sprintf("Performance duration must be positive for performance %s", item.ActivityName) } } // 2. validate PerformanceStartTime is the same or after StartTime if item.ActivityType == ActivityTypePerformance { startTime, err := time.Parse("15:04", item.StartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for performance %s", item.ActivityName) } else { performanceStartTime, err := time.Parse("15:04", *item.PerformanceStartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid performance start time format for performance %s", item.ActivityName) } else if performanceStartTime.Before(startTime) { result.IsValid = false result.ErrMsg = fmt.Sprintf("Performance start time %s is before start time %s for performance %s", performanceStartTime, item.StartTime, item.ActivityName) } } } // 3. validate PerformanceStartTime + Duration is at least 10 minutes before the StartTime of next PlanItem, if there is a next PlanItem if item.ActivityType == ActivityTypePerformance && i < len(in.PlanItems)-1 { performanceStartTime, err := time.Parse("15:04", *item.PerformanceStartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid performance start time format for performance %s", item.ActivityName) } else { endTime := performanceStartTime.Add(time.Duration(*item.Duration) * time.Minute) nextStartTime, err := time.Parse("15:04", in.PlanItems[i+1].StartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for next plan item %s", in.PlanItems[i+1].ActivityName) } else if !endTime.Add(10 * time.Minute).Before(nextStartTime) { result.IsValid = false result.ErrMsg = fmt.Sprintf("Performance %s ends too close to the start time of next plan item %s", item.ActivityName, in.PlanItems[i+1].ActivityName) } } } // 4. if it's an attraction, StartTime + QueueTime + Duration should be before the StartTime of next PlanItem, if there is a next PlanItem if item.ActivityType == ActivityTypeAttraction && i < len(in.PlanItems)-1 { startTime, err := time.Parse("15:04", item.StartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for attraction %s", item.ActivityName) } else { endTime := startTime.Add(time.Duration(*item.QueueTime) * time.Minute).Add(time.Duration(*item.Duration) * time.Minute) nextStartTime, err := time.Parse("15:04", in.PlanItems[i+1].StartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for next plan item %s", in.PlanItems[i+1].ActivityName) } else if !endTime.Before(nextStartTime) { result.IsValid = false result.ErrMsg = fmt.Sprintf("Attraction %s's queue time ends too close to the start time of next plan item %s", item.ActivityName, in.PlanItems[i+1].ActivityName) } } } // 新增校验:上一个 PlanItem 的结束时间距离下一个 PlanItem 的开始时间不能超过半小时 if i > 0 { prevItem := in.PlanItems[i-1] var prevEndTime, prevStartTime time.Time var parseErr error switch prevItem.ActivityType { case ActivityTypePerformance: prevStartTime, parseErr = time.Parse("15:04", *prevItem.PerformanceStartTime) if parseErr != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid performance start time format for previous plan item %s", prevItem.ActivityName) break } prevEndTime = prevStartTime.Add(time.Duration(*prevItem.Duration) * time.Minute) case ActivityTypeAttraction: prevStartTime, parseErr = time.Parse("15:04", prevItem.StartTime) if parseErr != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for previous attraction %s", prevItem.ActivityName) break } prevEndTime = prevStartTime.Add(time.Duration(*prevItem.QueueTime) * time.Minute).Add(10 * time.Minute) case ActivityTypeRestaurant: prevStartTime, parseErr = time.Parse("15:04", prevItem.StartTime) if parseErr != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for previous restaurant %s", prevItem.ActivityName) break } prevEndTime = prevStartTime.Add(45 * time.Minute) case ActivityTypeOther: prevStartTime, parseErr = time.Parse("15:04", prevItem.StartTime) if parseErr != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for previous move %s", prevItem.Location) break } prevEndTime = prevStartTime.Add(10 * time.Minute) } if parseErr == nil { currentStartTime, err := time.Parse("15:04", item.StartTime) if err != nil { result.IsValid = false result.ErrMsg = fmt.Sprintf("Invalid start time format for current plan item %s", item.ActivityName) } else if currentStartTime.Sub(prevEndTime) > 60*time.Minute { result.IsValid = false result.ErrMsg = fmt.Sprintf("The time gap between the end of previous plan item %s and the start of current plan item %s exceeds 60 minutes", prevItem.ActivityName, item.ActivityName) } } } if !result.IsValid { results = append(results, result) } } return &ValidatePlanItemsResponse{ ValidationResults: results, }, nil }