private Stream loadResult(TimetableOptionsModel model)
        {
            string path = getModelFilePath(model);

            FileStream fileStream = System.IO.File.Open(path, FileMode.Open);

            return(new GZipStream(fileStream, CompressionMode.Decompress));
        }
        private Stream loadResult(TimetableOptionsModel model)
        {
            string path = getModelFilePath(model);

            //These streams should be closed by ASP.NET when the response is returned
            FileStream fileStream = System.IO.File.Open(path, FileMode.Open);

            return(new GZipStream(fileStream, CompressionMode.Decompress));
        }
        private string getModelFilePath(TimetableOptionsModel model)
        {
            string fileName = string.Join("-", model.SubjectCodes);

            fileName += "_";

            fileName += model.LaterStarts ? "1" : "0";
            fileName += model.LessDays ? "1" : "0";

            return(Path.Combine(_hostingEnvironment.ContentRootPath, "TimetableCache", fileName));
        }
        //Return if the cached result was created within some timespan stretching back from today
        //If not, will delete the file off the disk also
        private bool checkCacheDate(TimetableOptionsModel model)
        {
            string path = getModelFilePath(model);

            var creationDate = System.IO.File.GetCreationTime(path);

            bool inDate = DateTime.Now - creationDate < TimeSpan.FromDays(1);

            if (!inDate)
            {
                System.IO.File.Delete(path);
            }

            return(inDate);
        }
        private string getModelFilePath(TimetableOptionsModel model)
        {
            //Order subject names alphabetically
            string fileName = string.Join("-", model.SubjectCodes.OrderBy(s => s));

            fileName += "_";

            fileName += model.LaterStarts ? "1" : "0";
            fileName += model.LessDays ? "1" : "0";

            //Remove illegal characters so that people can't navigate through directories
            Regex rgx = new Regex("[^a-zA-Z0-9 -]");

            fileName = rgx.Replace(fileName, "");

            string path = Path.Combine(_hostingEnvironment.ContentRootPath, "TimetableCache", fileName);

            return(path);
        }
        //Save our timetables to the disk so we don't have to perform the calculation again
        //Possibly dangerous if someone manages to come up with ~100,000 different subject options of significant size, but I find this unlikely
        //Otherwise this will fill up 10GB of disk and break whatever it's running on
        private void saveResult(TimetableOptionsModel model, TimetableBuildResultModel result)
        {
            string path = getModelFilePath(model);

            using (FileStream fileStream = System.IO.File.Open(path, FileMode.CreateNew))
            {
                using (GZipStream compressedStream = new GZipStream(fileStream, CompressionLevel.Optimal))
                {
                    using (StreamWriter writer = new StreamWriter(compressedStream))
                    {
                        JsonSerializerSettings jsonSettings = new JsonSerializerSettings
                        {
                            ContractResolver = new CamelCasePropertyNamesContractResolver()
                        };

                        writer.Write(JsonConvert.SerializeObject(result, jsonSettings));
                    }
                }
            }
        }
        public async Task <IActionResult> UpdateSelectedSubjects([FromBody] TimetableOptionsModel model)
        {
            if (model.SubjectCodes.Count > 4)
            {
                return(StatusCode(403));
            }

            List <Subject> subjects = await subjectsFromSubjectCodes(model.SubjectCodes);

            //No subjects? No possible timetables.
            if (!subjects.Any())
            {
                return(Json(0));
            }

            IEnumerable <ClassInfo> classInfos = subjects.SelectMany(subject => subject.ClassInfos);

            //Calculate the number of possible permutations
            long numPermutations = Generator.PossiblePermutationsCount(classInfos);

            return(Json(numPermutations));
        }
        public async Task <IActionResult> BuildTimetable([FromBody] TimetableOptionsModel model)
        {
            if (model.SubjectCodes.Count > 4)
            {
                return(StatusCode(403));
            }

            if (System.IO.File.Exists(getModelFilePath(model)))
            {
                return(File(loadResult(model), "application/json; charset=utf-8"));
            }

            Stopwatch responseStopwatch = new Stopwatch();

            responseStopwatch.Start();

            List <Subject> subjects = await subjectsFromSubjectCodes(model.SubjectCodes);

            IEnumerable <ClassInfo> classInfos         = subjects.SelectMany(subject => subject.ClassInfos);
            IEnumerable <ClassInfo> originalClassInfos = subjects.SelectMany(subject => subject.OriginalClassInfos);

            //Assign unique ids to each of the 'original' class infos, allowing for compression to work later
            int id = 0;

            foreach (var classInfo in originalClassInfos)
            {
                classInfo.ID = id++;
            }

            //Create a new generator with our sorting options and cancellation token
            Generator g = new Generator
            {
                SortLaterStarts   = model.LaterStarts,
                SortLessDays      = model.LessDays,
                CancellationToken = HttpContext.RequestAborted
            };

            long possiblePermutations = Generator.PossiblePermutationsCount(classInfos);

            List <Timetable> timetables = new List <Timetable>();

            //Check what algorithm to use, if we have over 5M permutations use the expanding algorithm
            if (possiblePermutations > 5 * 1000 * 1000)
            {
                timetables = g.GeneratePermutationsExpanding(classInfos);
            }
            else
            {
                timetables = g.GenerateTimetablesBruteForce(classInfos);
            }

            //Take 25,000 of our timetables and compress them
            var compressedTimetables = timetables.Take(25000).Select(t => new CompressedTimetable(t)).ToList();

            var result = new TimetableBuildResultModel(compressedTimetables, originalClassInfos.ToList());

            responseStopwatch.Stop();

            //Only save results for meaningfully long requests (10secs)
            if (responseStopwatch.ElapsedMilliseconds > 10 * 1000)
            {
                saveResult(model, result);
            }

            return(Json(result));
        }
        public async Task <IActionResult> BuildTimetable([FromBody] TimetableOptionsModel model)
        {
            if (model.SubjectCodes.Count > 5)
            {
                return(Json(new TimetableBuildResultModel("Too many subjects selected.")));
            }

            if (System.IO.File.Exists(getModelFilePath(model)) && checkCacheDate(model))
            {
                return(File(loadResult(model), "application/json; charset=utf-8"));
            }

            List <Subject> subjects = await subjectsFromSubjectCodes(model.SubjectCodes);

            IEnumerable <ClassInfo> classInfos         = subjects.SelectMany(subject => subject.ClassInfos);
            IEnumerable <ClassInfo> originalClassInfos = subjects.SelectMany(subject => subject.OriginalClassInfos);

            var allScheduledClasses = classInfos.SelectMany(ci => ci.AllScheduledClasses);


            //Create a new generator with our sorting options and cancellation token
            Generator g = new Generator
            {
                SortLaterStarts   = model.LaterStarts,
                SortLessDays      = model.LessDays,
                CancellationToken = HttpContext.RequestAborted
            };

            long possiblePermutations = Generator.PossiblePermutationsCount(classInfos);

            //Trying to generate when there's more than a trillion possibilities is a bad idea
            if (possiblePermutations > (long)1000 * 1000 * 1000 * 1000)
            {
                return(Json(new TimetableBuildResultModel("Can't generate timetables: too many possible timetables.")));
            }


            List <Timetable> timetables = new List <Timetable>();

            //Check what algorithm to use, if we have over 5M permutations use the expanding algorithm
            if (possiblePermutations > 5 * 1000 * 1000)
            {
                timetables = g.GeneratePermutationsExpanding(classInfos);
            }
            else
            {
                timetables = g.GenerateTimetablesBruteForce(classInfos);
            }

            int numberGenerated = timetables.Count;

            //Take 25,000 timetables and compress them
            var result = new TimetableBuildResultModel(timetables.Take(25000).ToList(),
                                                       numberGenerated,
                                                       allScheduledClasses.ToList(),
                                                       originalClassInfos.ToList());

            saveResult(model, result);

            return(Json(result));
        }