func (s *Scheduler) NextRun() (*Job, time.Time) {
	if len(s.Jobs()) <= 0 {
		return nil, s.now()
	}
	sort.Sort(s)
	return s.Jobs()[0], s.Jobs()[0].NextRun()
}
NextRun datetime when the next Job should run.
package gocron
import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"sync"
	"time"
	"github.com/robfig/cron/v3"
	"golang.org/x/sync/semaphore"
)
type limitMode int8
type Scheduler struct {
	jobsMutex sync.RWMutex
	jobs      []*Job
	locationMutex sync.RWMutex
	location      *time.Location
	runningMutex  sync.RWMutex
	running       bool  
	time     timeWrapper  
	executor *executor    
	tags sync.Map  
	tagsUnique      bool  
	updateJob       bool  
	waitForInterval bool  
	jobCreated      bool  
}
const allWeekDays = 7
func NewScheduler(loc *time.Location) *Scheduler {
	executor := newExecutor()
	return &Scheduler{
		jobs:       make([]*Job, 0),
		location:   loc,
		running:    false,
		time:       &trueTime{},
		executor:   &executor,
		tagsUnique: false,
	}
}
func (s *Scheduler) SetMaxConcurrentJobs(n int, mode limitMode) {
	s.executor.maxRunningJobs = semaphore.NewWeighted(int64(n))
	s.executor.limitMode = mode
}
func (s *Scheduler) StartBlocking() {
	s.StartAsync()
	<-make(chan bool)
}
func (s *Scheduler) StartAsync() {
	if !s.IsRunning() {
		s.start()
	}
}
func (s *Scheduler) start() {
	go s.executor.start()
	s.setRunning(true)
	s.runJobs(s.Jobs())
}
func (s *Scheduler) runJobs(jobs []*Job) {
	for _, job := range jobs {
		s.scheduleNextRun(job)
	}
}
func (s *Scheduler) setRunning(b bool) {
	s.runningMutex.Lock()
	defer s.runningMutex.Unlock()
	s.running = b
}
func (s *Scheduler) IsRunning() bool {
	s.runningMutex.RLock()
	defer s.runningMutex.RUnlock()
	return s.running
}
func (s *Scheduler) Jobs() []*Job {
	s.jobsMutex.RLock()
	defer s.jobsMutex.RUnlock()
	return s.jobs
}
func (s *Scheduler) setJobs(jobs []*Job) {
	s.jobsMutex.Lock()
	defer s.jobsMutex.Unlock()
	s.jobs = jobs
}
func (s *Scheduler) Len() int {
	s.jobsMutex.RLock()
	defer s.jobsMutex.RUnlock()
	return len(s.jobs)
}
func (s *Scheduler) Swap(i, j int) {
	s.jobsMutex.Lock()
	defer s.jobsMutex.Unlock()
	s.jobs[i], s.jobs[j] = s.jobs[j], s.jobs[i]
}
func (s *Scheduler) Less(first, second int) bool {
	return s.Jobs()[second].NextRun().Unix() >= s.Jobs()[first].NextRun().Unix()
}
func (s *Scheduler) ChangeLocation(newLocation *time.Location) {
	s.locationMutex.Lock()
	defer s.locationMutex.Unlock()
	s.location = newLocation
}
func (s *Scheduler) Location() *time.Location {
	s.locationMutex.RLock()
	defer s.locationMutex.RUnlock()
	return s.location
}
type nextRun struct {
	duration time.Duration
	dateTime time.Time
}
func (s *Scheduler) scheduleNextRun(job *Job) {
	now := s.now()
	lastRun := job.LastRun()
	if !s.jobPresent(job) {
		return
	}
	if job.getStartsImmediately() {
		s.run(job)
		lastRun = now
		job.setStartsImmediately(false)
	}
	if job.neverRan() {
		if !job.startAtTime.IsZero() && job.startAtTime.Before(now) {
			duration := s.durationToNextRun(job.startAtTime, job).duration
			job.startAtTime = job.startAtTime.Add(duration)
			if job.startAtTime.Before(now) {
				diff := now.Sub(job.startAtTime)
				duration := s.durationToNextRun(job.startAtTime, job).duration
				count := diff / duration
				if diff%duration != 0 {
					count++
				}
				job.startAtTime = job.startAtTime.Add(duration * count)
			}
		}
		lastRun = now
	}
	if !job.shouldRun() {
		s.RemoveByReference(job)
		return
	}
	next := s.durationToNextRun(lastRun, job)
	if next.dateTime.IsZero() {
		job.setNextRun(lastRun.Add(next.duration))
	} else {
		job.setNextRun(next.dateTime)
	}
	job.setTimer(time.AfterFunc(next.duration, func() {
		if !next.dateTime.IsZero() {
			for {
				if time.Now().Unix() >= next.dateTime.Unix() {
					break
				}
			}
		}
		s.run(job)
		s.scheduleNextRun(job)
	}))
}
func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) nextRun {
	if job.getStartAtTime().After(lastRun) {
		return nextRun{duration: job.getStartAtTime().Sub(s.now()), dateTime: job.getStartAtTime()}
	}
	var next nextRun
	switch job.getUnit() {
	case milliseconds, seconds, minutes, hours:
		next.duration = s.calculateDuration(job)
	case days:
		next = s.calculateDays(job, lastRun)
	case weeks:
		if len(job.scheduledWeekdays) != 0 {  
			next = s.calculateWeekday(job, lastRun)
		} else {
			next = s.calculateWeeks(job, lastRun)
		}
	case months:
		next = s.calculateMonths(job, lastRun)
	case duration:
		next.duration = job.getDuration()
	case crontab:
		next.dateTime = job.cronSchedule.Next(lastRun)
		next.duration = next.dateTime.Sub(lastRun)
	}
	return next
}
func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun {
	lastRunRoundedMidnight := s.roundToMidnight(lastRun)
	if len(job.daysOfTheMonth) != 0 {  
		nextRunDateMap := make(map[int]nextRun)
		for _, day := range job.daysOfTheMonth {
			nextRunDateMap[day] = calculateNextRunForMonth(s, job, lastRun, day)
		}
		nextRunResult := nextRun{}
		for _, val := range nextRunDateMap {
			if nextRunResult.dateTime.IsZero() {
				nextRunResult = val
			} else if nextRunResult.dateTime.Sub(val.dateTime).Milliseconds() > 0 {
				nextRunResult = val
			}
		}
		return nextRunResult
	}
	next := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, job.interval, 0)
	return nextRun{duration: until(lastRunRoundedMidnight, next), dateTime: next}
}
func calculateNextRunForMonth(s *Scheduler, job *Job, lastRun time.Time, dayOfMonth int) nextRun {
	jobDay := time.Date(lastRun.Year(), lastRun.Month(), dayOfMonth, 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
	difference := absDuration(lastRun.Sub(jobDay))
	next := lastRun
	if jobDay.Before(lastRun) {  
		next = next.AddDate(0, job.interval, -0)
		next = next.Add(-difference)
	} else {
		if job.interval == 1 {  
			next = next.AddDate(0, job.interval-1, 0)
		} else {  
			next = next.AddDate(0, job.interval, 0)
		}
		next = next.Add(difference)
	}
	return nextRun{duration: until(lastRun, next), dateTime: next}
}
func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) nextRun {
	daysToWeekday := remainingDaysToWeekday(lastRun.Weekday(), job.Weekdays())
	totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job)
	next := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference)
	return nextRun{duration: until(lastRun, next), dateTime: next}
}
func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) nextRun {
	totalDaysDifference := int(job.interval) * 7
	next := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference)
	return nextRun{duration: until(lastRun, next), dateTime: next}
}
func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekday int, job *Job) int {
	if job.interval > 1 && job.RunCount() < len(job.Weekdays()) {  
		return daysToWeekday
	} else if job.interval > 1 && job.RunCount() >= len(job.Weekdays()) {
		if daysToWeekday > 0 {
			return int(job.interval)*7 - (allWeekDays - daysToWeekday)
		}
		return int(job.interval) * 7
	}
	if daysToWeekday == 0 {  
		lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
		if lastRun.Before(lastRunAtTime) || lastRun.Equal(lastRunAtTime) {
			return 0
		}
		return 7
	}
	return daysToWeekday
}
func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun {
	if job.interval == 1 {
		lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
		lastRunUnix, nowUnix := job.LastRun().Unix(), s.now().Unix()
		if lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
			lastRun = lastRunDayPlusJobAtTime
		}
		if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) {
			return nextRun{duration: until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime())), dateTime: s.roundToMidnight(lastRun).Add(job.getAtTime())}
		}
	}
	nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, job.interval).In(s.Location())
	return nextRun{duration: until(lastRun, nextRunAtTime), dateTime: nextRunAtTime}
}
func until(from time.Time, until time.Time) time.Duration {
	return until.Sub(from)
}
func shouldRunToday(lastRun time.Time, atTime time.Time) bool {
	return lastRun.Before(atTime)
}
func in(scheduleWeekdays []time.Weekday, weekday time.Weekday) bool {
	in := false
	for _, weekdayInSchedule := range scheduleWeekdays {
		if int(weekdayInSchedule) == int(weekday) {
			in = true
			break
		}
	}
	return in
}
func (s *Scheduler) calculateDuration(job *Job) time.Duration {
	lastRun := job.LastRun()
	if job.neverRan() && shouldRunAtSpecificTime(job) {  
		atTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
		if lastRun.Before(atTime) || lastRun.Equal(atTime) {
			return time.Until(s.roundToMidnight(lastRun).Add(job.getAtTime()))
		}
	}
	interval := job.interval
	switch job.getUnit() {
	case milliseconds:
		return time.Duration(interval) * time.Millisecond
	case seconds:
		return time.Duration(interval) * time.Second
	case minutes:
		return time.Duration(interval) * time.Minute
	default:
		return time.Duration(interval) * time.Hour
	}
}
func shouldRunAtSpecificTime(job *Job) bool {
	return job.getAtTime() != 0
}
func remainingDaysToWeekday(from time.Weekday, weekDays []time.Weekday) int {
	var (
		daysUntilScheduledDay         int
		daysUntilScheduledDayPositive = allWeekDays
		daysUntilScheduledDayNegative = 0
	)
	for _, day := range weekDays {
		differenceBetweenDays := int(day) - int(from)
		if differenceBetweenDays > 0 && differenceBetweenDays < daysUntilScheduledDayPositive {
			daysUntilScheduledDayPositive = differenceBetweenDays
		}
		if differenceBetweenDays < 0 && differenceBetweenDays < daysUntilScheduledDayNegative {
			daysUntilScheduledDayNegative = differenceBetweenDays
		}
	}
	if daysUntilScheduledDayPositive > 0 && daysUntilScheduledDayPositive != allWeekDays {
		daysUntilScheduledDay = daysUntilScheduledDayPositive
	} else if daysUntilScheduledDayNegative < 0 {
		daysUntilScheduledDay = allWeekDays + daysUntilScheduledDayNegative
	}
	return daysUntilScheduledDay
}
func absDuration(a time.Duration) time.Duration {
	if a >= 0 {
		return a
	}
	return -a
}
func (s *Scheduler) roundToMidnight(t time.Time) time.Time {
	return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.Location())
}