internal static IEnumerable <EfficiencyShift> GetOrphanShifts([NotNull] Func <EfficiencyShift> shiftFactory,
                                                                      [NotNull] IEnumerable <EfficiencyTimeSheet> timeSheets, [NotNull] IEnumerable <Transaction> transactions,
                                                                      TimeSpan?startPadding = null, TimeSpan?endPadding = null)
        {
            var timeSheetEnumerator = new SafeEnumerator <EfficiencyTimeSheet>(timeSheets.GetEnumerator());

            timeSheetEnumerator.MoveNext();

            // Track the current time sheet globally so we can
            // detect when it changes.
            EfficiencyTimeSheet currentTimeSheet = null;
            var shift           = (EfficiencyShift)null;
            var lastTransaction = (Transaction)null;

            var startPad = startPadding.GetValueOrDefault(TimeSpan.FromMinutes(30));
            var endPad   = endPadding.GetValueOrDefault(TimeSpan.FromMinutes(30));

            foreach (var transaction in transactions)
            {
                if (transaction.TransactionDate < lastTransaction?.TransactionDate)
                {
                    throw new ArgumentException(
                              $"Transactions must be ordered by {nameof(Transaction.TransactionDate)}",
                              nameof(transactions));
                }

                var advanceResult     = AdvanceTimeSheet(timeSheetEnumerator, transaction);
                var previousTimeSheet = advanceResult.PreviousTimeSheet;
                var nextTimeSheet     = advanceResult.TimeSheet;

                // If the time sheet changed it means we moved past its end date and are either:
                //   1) Within a new time sheet
                //   2) In a new gap
                // Either way we want to close off the old shift if we had one.
                // If it didn't change but we are now past the start time of the current
                // time sheet also close off the shift.
                var changed    = nextTimeSheet != currentTimeSheet;
                var overlaps   = nextTimeSheet?.PunchInTime <= transaction.TransactionDate;
                var closeShift = changed || transaction.TransactionDate - lastTransaction?.TransactionDate > TimeSpan.FromHours(8) || overlaps;
                currentTimeSheet = nextTimeSheet;

                if (shift != null && closeShift)
                {
                    shift.EndTime = lastTransaction.TransactionDate.Ceiling(endPad);

                    if (shift.EndTime > currentTimeSheet?.PunchInTime)
                    {
                        shift.EndTime = currentTimeSheet.PunchInTime;
                    }

                    yield return(shift);

                    shift = null;
                }

                var punchInTime = currentTimeSheet?.PunchInTime ?? DateTime.MaxValue;

                if (shift == null && transaction.TransactionDate < punchInTime)
                {
                    shift           = shiftFactory();
                    shift.StartTime = transaction.TransactionDate.Floor(startPad);

                    if (shift.StartTime < previousTimeSheet?.PunchOutTime)
                    {
                        shift.StartTime = previousTimeSheet.PunchOutTime.Value;
                    }

                    shift.OperationalDate = transaction.OperationalDate.Date;
                }

                lastTransaction = transaction;
            }

            if (shift == null)
            {
                yield break;
            }

            shift.EndTime = lastTransaction.TransactionDate.Ceiling(endPad);

            if (shift.EndTime > currentTimeSheet?.PunchInTime)
            {
                shift.EndTime = currentTimeSheet.PunchInTime;
            }

            yield return(shift);
        }
        /// <summary>
        /// Advances the time sheet enumerator so that the current time sheet's punchOutTime > transactionDate.
        /// If no more time sheets exist that meet that criteria null is returned.
        /// </summary>
        /// <param name="enumerator"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        internal static (EfficiencyTimeSheet TimeSheet, EfficiencyTimeSheet PreviousTimeSheet) AdvanceTimeSheet([NotNull] SafeEnumerator <EfficiencyTimeSheet> enumerator, Transaction transaction)
        {
            var transactionDate = transaction.TransactionDate;

            if (enumerator.Current?.PunchOutTime == null || enumerator.Current?.PunchOutTime >= transactionDate)
            {
                return(enumerator.Current, enumerator.Previous);
            }

            while (enumerator.MoveNext())
            {
                var current = enumerator.Current;

                if (current == null)
                {
                    continue;
                }

                if (current.PunchInTime > current.PunchOutTime)
                {
                    throw new InvalidOperationException(
                              $"Invalid TimeSheet. {nameof(EfficiencyTimeSheet.PunchInTime)} > {nameof(EfficiencyTimeSheet.PunchOutTime)}");
                }

                if (enumerator.Previous?.PunchInTime > current?.PunchInTime)
                {
                    throw new ArgumentException("TimeSheets must be ordered by PunchInTime",
                                                nameof(enumerator));
                }

                // We found the next time sheet. Break out.
                if (current.PunchOutTime == null || current.PunchOutTime >= transactionDate)
                {
                    break;
                }
            }

            return(enumerator.Current, enumerator.Previous);
        }