/// <summary> /// Finds the most frequent commercial type allocated to the break /// </summary> /// <param name="breakCommercials">Break/commercials instance</param> /// <returns>The commercial type and the frequency it occurs</returns> private (string CommercialType, int Frequency) GetMostFrequentCommercialType( BreakCommercials breakCommercials) { var commercialFrequency = breakCommercials.Commercials .GroupBy(c => c.CommercialType) .Select( g => new { CommercialType = g.Key, Count = g.Select(c => c.CommercialType).Count() }); var maxValue = commercialFrequency.First( value1 => value1.Count == commercialFrequency.Max(value2 => value2.Count)); return(maxValue.CommercialType, maxValue.Count); }
/// <summary> /// For a specific commercial swap this method calculates what the affect on /// the total rating will be, so we can determine if it's better than other /// available swaps /// </summary> /// <param name="sourceBreakCommercials">Source break/commercials instance</param> /// <param name="sourceCommercial">Source commercial</param> /// <param name="targetBreakCommercials">Target break/commercials instance</param> /// <param name="targetCommercial">Target commercial</param> /// <returns>The rating change</returns> private int GetRatingChangeForSwap( BreakCommercials sourceBreakCommercials, Commercial sourceCommercial, BreakCommercials targetBreakCommercials, Commercial targetCommercial) { var ratingTotalAfterSwap = 0; if (sourceBreakCommercials != null) { ratingTotalAfterSwap += sourceBreakCommercials.Break.BreakDemographics .Where(value => value.Demographic.Id == targetCommercial.Demographic.Id) .Select(value => value.Rating).FirstOrDefault(); } if (targetBreakCommercials != null) { ratingTotalAfterSwap += targetBreakCommercials.Break.BreakDemographics .Where(value => value.Demographic.Id == sourceCommercial.Demographic.Id) .Select(value => value.Rating).FirstOrDefault(); } var ratingTotalBeforeSwap = 0; if (sourceBreakCommercials != null) { ratingTotalBeforeSwap += sourceBreakCommercials.Break.BreakDemographics .Where(value => value.Demographic.Id == sourceCommercial.Demographic.Id) .Select(value => value.Rating).FirstOrDefault(); } if (targetBreakCommercials != null) { ratingTotalBeforeSwap += targetBreakCommercials.Break.BreakDemographics .Where(value => value.Demographic.Id == targetCommercial.Demographic.Id) .Select(value => value.Rating).FirstOrDefault(); } return(ratingTotalAfterSwap - ratingTotalBeforeSwap); }
/// <summary> /// Checks if the commercial can be moved to the break based on commercial type /// restrictions. /// </summary> /// <param name="breakCommercials">The Break/commercials instance</param> /// <param name="commercial">The commercial to add</param> /// <returns>True if the commercial can be added to the break</returns> private bool IsCommercialValidForBreak( BreakCommercials breakCommercials, Commercial commercial) { //Is the source commercial valid for the target break? if (breakCommercials.Break.InvalidCommercialTypes != null && breakCommercials.Break.InvalidCommercialTypes.Contains(commercial.CommercialType)) { //Commercial type is invalid for this break return(false); } var targetCountAlreadyInBreak = breakCommercials.Commercials.Count( value => value.CommercialType == commercial.CommercialType); if (targetCountAlreadyInBreak + 1 > MaxBreakCapacityForCommercialType(breakCommercials.Break)) { //Adding this type to the break will mean there's too many of the type return(false); } return(true); }
/// <summary> /// Resolves invalid commercial allocations by attempting swaps with other breaks /// </summary> private void SwapInvalidCommercials( List <BreakCommercials> allBreakCommercials, List <Commercial> unusedCommercials) { foreach (var breakCommercials in allBreakCommercials) { //If there's any commercials of an invalid type, they have to go if (breakCommercials.Break.InvalidCommercialTypes != null) { var invalidCommercials = breakCommercials.Commercials.Where( value => breakCommercials.Break.InvalidCommercialTypes.Contains(value.CommercialType)).ToList(); foreach (var invalidCommercial in invalidCommercials) { var bestSwapResult = GetBestSwap( breakCommercials, invalidCommercial, allBreakCommercials.Where( value => value.Break.Id != breakCommercials.Break.Id).ToList(), unusedCommercials); if (bestSwapResult.HasValue) { var selectedBreakCommercials = bestSwapResult.Value.SelectedBreakCommercials; var selectedTargetCommercial = bestSwapResult.Value.NewSourceCommercial; selectedBreakCommercials.Commercials.Remove(selectedTargetCommercial); selectedBreakCommercials.Commercials.Add(invalidCommercial); breakCommercials.Commercials.Remove(invalidCommercial); breakCommercials.Commercials.Add(selectedTargetCommercial); Debug.WriteLine($"Swapped {invalidCommercial.Id} for {selectedTargetCommercial.Id}"); } else { throw new ArgumentException( "Unable to fill the breaks with the available commercials - cannot reallocate invalid commercial"); } } } //Next check if there's too many commercials of a certain type //(e.g. if we have 4 slots and 3 of a certain type, there's no way //to arrange them so they aren't adjacent). In this case we will go through //comparing the rating change for the potential swaps and pick the most favourable one var frequentCommercialResult = GetMostFrequentCommercialType(breakCommercials); string tooFrequentCommercialType = null; int maxFrequencyAllowed = MaxBreakCapacityForCommercialType(breakCommercials.Break); if (frequentCommercialResult.Frequency > maxFrequencyAllowed) { tooFrequentCommercialType = frequentCommercialResult.CommercialType; } if (!string.IsNullOrEmpty(tooFrequentCommercialType)) { var matchingCommercials = breakCommercials.Commercials.Where( value => value.CommercialType == tooFrequentCommercialType).ToList(); while (matchingCommercials.Count > maxFrequencyAllowed) { var bestRatingChange = int.MinValue; BreakCommercials selectedBreakCommercials = null; Commercial oldSourceCommercial = null; Commercial newSourceCommercial = null; Commercial oldTargetCommercial = null; Commercial newTargetCommercial = null; foreach (var matchingCommercial in matchingCommercials) { var bestSwapResult = GetBestSwap( breakCommercials, matchingCommercial, allBreakCommercials.Where( value => value.Break.Id != breakCommercials.Break.Id).ToList(), unusedCommercials); if (bestSwapResult.HasValue && bestSwapResult.Value.RatingChange > bestRatingChange) { oldSourceCommercial = matchingCommercial; newSourceCommercial = bestSwapResult.Value.NewSourceCommercial; oldTargetCommercial = bestSwapResult.Value.OldTargetCommercial; newTargetCommercial = bestSwapResult.Value.NewTargetCommercial; selectedBreakCommercials = bestSwapResult.Value.Item1; bestRatingChange = bestSwapResult.Value.RatingChange; if (oldSourceCommercial != newSourceCommercial) { Debug.WriteLine( $"Swapped {oldSourceCommercial.Id} for {newSourceCommercial.Id}"); } if (oldTargetCommercial != newTargetCommercial) { Debug.WriteLine( $"Swapped {oldTargetCommercial.Id} for {newTargetCommercial.Id}"); } } } //If we have a valid swap setup, do it if (newSourceCommercial == null || newSourceCommercial == oldSourceCommercial) { throw new ArgumentException( "Unable to fill the breaks with the available commercials - cannot reallocate too-frequent commercial"); } breakCommercials.Commercials.Remove(oldSourceCommercial); breakCommercials.Commercials.Add(newSourceCommercial); selectedBreakCommercials.Commercials.Remove(oldTargetCommercial); selectedBreakCommercials.Commercials.Add(newTargetCommercial); matchingCommercials.Remove(oldSourceCommercial); //Ensure we add any now-unused commercials back to the unused collection if (oldSourceCommercial != newSourceCommercial && oldSourceCommercial != newTargetCommercial) { unusedCommercials.Add(oldSourceCommercial); } if (oldTargetCommercial != newSourceCommercial && oldTargetCommercial != newTargetCommercial) { unusedCommercials.Add(oldTargetCommercial); } } } } }
GetBestSwap( BreakCommercials sourceBreakCommercials, Commercial sourceCommercial, List <BreakCommercials> otherBreakCommercials, List <Commercial> unusedCommercials) { var bestRatingChange = int.MinValue; BreakCommercials selectedBreakCommercials = null; Commercial selectedOldTargetCommercial = null; Commercial selectedNewTargetCommercial = null; Commercial selectedNewSourceCommercial = sourceCommercial; //Check the rating change if we swap the source commercial with an unused one foreach (var unusedCommercial in unusedCommercials) { var ratingChange = GetRatingChangeForSwap( sourceBreakCommercials, sourceCommercial, null, unusedCommercial); if (ratingChange > bestRatingChange) { bestRatingChange = ratingChange; selectedNewSourceCommercial = unusedCommercial; } } //Create list of possible swaps, ignoring commercials which are invalid //or of the same type foreach (var breakCommercials in otherBreakCommercials) { //Is the source commercial valid for the target break? if (!IsCommercialValidForBreak(breakCommercials, sourceCommercial)) { continue; } foreach (var targetCommercial in breakCommercials.Commercials) { //Is the target commercial valid for the source break? if (!IsCommercialValidForBreak(sourceBreakCommercials, targetCommercial)) { continue; } //What's the rating total change after the swap? var ratingChange = GetRatingChangeForSwap( sourceBreakCommercials, sourceCommercial, breakCommercials, targetCommercial); //Check the rating change if we swap the target commercial with an unused one Commercial selectedUnusedCommercial = null; int unusedCommercialRatingChange = int.MinValue; foreach (var unusedCommercial in unusedCommercials) { var unusedRatingChange = GetRatingChangeForSwap( null, unusedCommercial, breakCommercials, targetCommercial); if (unusedRatingChange > unusedCommercialRatingChange) { unusedCommercialRatingChange = unusedRatingChange; selectedUnusedCommercial = unusedCommercial; } } if (unusedCommercialRatingChange > 0) { ratingChange += unusedCommercialRatingChange; } if (bestRatingChange < ratingChange) { bestRatingChange = ratingChange; selectedBreakCommercials = breakCommercials; selectedNewSourceCommercial = targetCommercial; selectedOldTargetCommercial = targetCommercial; if (unusedCommercialRatingChange > 0) { selectedNewTargetCommercial = selectedUnusedCommercial; } else { selectedNewTargetCommercial = sourceCommercial; } } } } return( selectedBreakCommercials, selectedNewSourceCommercial, selectedOldTargetCommercial, selectedNewTargetCommercial, bestRatingChange); }