/// <summary> /// Returns whether or not another <see cref="DateTimeSpan"/> in any way overlaps this /// <see cref="DateTimeSpan"/>. /// </summary> /// <param name="span">The DateTimeSpan to test for any overlaps.</param> public bool Overlaps(DateTimeSpan span) { return(span.Equals(this) || (span.Start < End && span.End == End) || (span.End > Start && span.Start == Start) || (span.Contains(Start) && Contains(span.End)) || (span.Contains(End) && Contains(span.Start)) || (span.Contains(Start) && span.Contains(End)) || (Contains(span.Start) && Contains(span.End))); }
/// <summary> /// Tries to find and return all items of a product type during a timespan that aren't reserved. /// </summary> /// <param name="productId">The product type whose items to return.</param> /// <param name="span">The time period to test for unreserved items.</param> /// <returns>A subset of all items of the given product type, or null if the product type has no items.</returns> private List <ProductItem> Core_GetUnreservedItems(string productId, DateTimeSpan span) { List <ProductItem> items = Core_getProductItems(new string[] { productId })[productId].ToList(); if (!items.Any()) { return(null); } var condition = new MySqlConditionBuilder(); condition.NewGroup(); foreach (var item in items) { condition.Or() .Column("product_item") .Equals(item.Id, item.GetIndex("PRIMARY").Columns[0].Type); } condition.EndGroup(); condition.And() .Column("end") .GreaterThanOrEqual() .Operand(span.Start, MySqlDbType.DateTime); condition.And() .Column("start") .LessThanOrEqual() .Operand(span.End, MySqlDbType.DateTime); List <LoanItem> loans = Connection.Select <LoanItem>(condition).ToList(); foreach (var loan in loans) { if (!items.Any(x => x.Id == loan.ProductItem)) { continue; } var loanSpan = new DateTimeSpan(loan.Start, loan.End); if (span.Overlaps(loanSpan)) { items.Remove(items.First(x => x.Id == loan.ProductItem)); } } return(items); }
public JObject resizeLoan(JObject request) { //Get arguments request.TryGetValue("loanId", out JToken loanItemID); request.TryGetValue("start", out JToken requestStart); request.TryGetValue("end", out JToken requestEnd); // Verify the arguments List <string> failedVerifications = new List <string>(); if (loanItemID == null || loanItemID.Type != JTokenType.Integer) { failedVerifications.Add("loanId"); } if (requestStart == null || !(requestStart.Type == JTokenType.String || requestStart.Type == JTokenType.Date)) { failedVerifications.Add("start"); } if (requestEnd == null || !(requestEnd.Type == JTokenType.String || requestEnd.Type == JTokenType.Date)) { failedVerifications.Add("end"); } if (failedVerifications.Any()) { return(Templates.InvalidArguments(failedVerifications.ToArray())); } // Parse arguments DateTime start; DateTime end; string loanID = loanItemID.ToString(); try { start = DateTime.Parse(requestStart.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'start'")); } try { end = DateTime.Parse(requestEnd.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'end'")); } if (end < start) { return(Templates.InvalidArguments("'end' must come after 'start'")); } DateTimeSpan newLoanSpan = new DateTimeSpan(start, end); if (newLoanSpan.Start < DateTime.Now.Date) { return(Templates.InvalidArgument("'start' may not be set earlier than today.")); } if (newLoanSpan.Duration > MaxLoanDuration) { return(Templates.InvalidArgument($"Duration of the loan may not exceed {MaxLoanDuration.Days} days.")); } // Build a condition to get the specific loan var condition = new MySqlConditionBuilder(); // Automatically limit results to current user only if their permission is User if (CurrentUser.Permission <= UserPermission.User) { condition.And().Column("user").Equals(CurrentUser.Username, MySqlDbType.String); } condition.And().Column("id").Equals(loanID, MySqlDbType.Int32); //Get the specified loanItem if it exists. If it doesn't, throw an error. LoanItem oldLoan = Connection.Select <LoanItem>(condition).FirstOrDefault(); if (oldLoan == null) { return(Templates.NoSuchLoan(loanID)); } // Return a loanExpired template if the loan has already ended if (oldLoan.End < DateTime.Now) { return(Templates.LoanExpired()); } if (oldLoan.Start < DateTime.Now && oldLoan.Start != start) { return(Templates.LoanAlreadyStarted()); } // Build condition condition = new MySqlConditionBuilder() .Column("product_item") .Equals(oldLoan.ProductItem, MySqlDbType.Int32); // Select only relevant loans condition.And().Column("end").GreaterThanOrEqual().Operand(start, MySqlDbType.DateTime); condition.And().Column("start").LessThanOrEqual().Operand(end, MySqlDbType.DateTime); //Get all loanItems for this loan's product_item. var loans = Connection.Select <LoanItem>(condition).ToList(); //Check for conflicting loanItems bool canResize = true; foreach (var loan in loans) { var loanSpan = new DateTimeSpan(loan.Start, loan.End); if (newLoanSpan.Overlaps(loanSpan)) { canResize = false; break; } } //Create response var responseData = new JObject(); JObject response = new JObject() { { "reason", null }, { "responseData", responseData } }; if (canResize) { // Update loan oldLoan.Start = start; oldLoan.End = end; Connection.Update(oldLoan); } else if (oldLoan.IsAcquired && !canResize) // If loan is not eligible for a item reassignment { return(Templates.LoanResizeFailed()); } else // If the loan is eligible for an item reassignment { // Try to retrieve a list of unreserved equivalent products var unreservedAlts = Core_GetUnreservedItems(oldLoan.GetProductItem(Connection).ProductId, newLoanSpan); if (unreservedAlts == null) // Normally this value should not be able to be null. This block is just a failsafe that adds context. { throw new InvalidOperationException("Expected type of 'unreservedAlts' is List, but nullreference was found."); } if (!unreservedAlts.Any()) { Log.Error("Failed at line 107"); return(Templates.LoanResizeFailed()); } // Update loan oldLoan.ProductItem = unreservedAlts.First().Id.Value; oldLoan.Start = start; oldLoan.End = end; Connection.Update(oldLoan); // Return new data in response and add message for context responseData["product_item"] = oldLoan.ProductItem; response["message"] = "Loan has been reassigned."; } return(response); }
public JObject addLoan(JObject request) { //Get arguments request.TryGetValue("productId", out JToken requestProductId); request.TryGetValue("start", out JToken requestStart); request.TryGetValue("end", out JToken requestEnd); // Verify the arguments List <string> failedVerifications = new List <string>(); if (requestProductId == null || requestProductId.Type != JTokenType.String) { failedVerifications.Add("productID"); } if (requestStart == null || !(requestStart.Type == JTokenType.String || requestStart.Type == JTokenType.Date)) { failedVerifications.Add("start"); } if (requestEnd == null || !(requestEnd.Type == JTokenType.String || requestEnd.Type == JTokenType.Date)) { failedVerifications.Add("end"); } if (failedVerifications.Any()) { return(Templates.InvalidArguments(failedVerifications.ToArray())); } // Parse arguments DateTime start; DateTime end; string productId = requestProductId.ToString(); try { start = DateTime.Parse(requestStart.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'start'")); } try { end = DateTime.Parse(requestEnd.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'end'")); } if (end < start) { return(Templates.InvalidArguments("'end' must come after 'start'")); } var newLoanSpan = new DateTimeSpan(start, end); if (newLoanSpan.Start < DateTime.Now.Date) { return(Templates.InvalidArgument("'start' may not be set earlier than today.")); } if (newLoanSpan.Duration > MaxLoanDuration) { return(Templates.InvalidArgument($"Duration of the loan may not exceed {MaxLoanDuration.Days} days.")); } // Get an unreserved product item ProductItem item; try { item = Core_GetUnreservedItems(productId, newLoanSpan).FirstOrDefault(); } catch (Exception) { return(Templates.NoItemsForProduct(productId)); } if (item == null) { return(Templates.NoItemsForProduct($"Product '{productId}' has no items available during this time.")); } var loan = new LoanItem(null, CurrentUser.Username, item.Id.Value, start, end); Connection.Upload(loan); //Create response JObject response = new JObject() { { "reason", null }, { "responseData", new JObject() { { "loanId", loan.Id }, { "productItem", item.Id } } } }; return(response); }
public JObject getProductAvailability(JObject request) { //Get arguments request.TryGetValue("products", out JToken productValue); if (productValue == null || (productValue.Type != JTokenType.String && productValue.Type != JTokenType.Array)) { return(Templates.MissingArguments("statType")); } //Parse arguments List <string> productIDs = new List <string>(); if (productValue.Type == JTokenType.String) { //TODO Allow * to be used as value, selecting all products productIDs.Add(productValue.ToObject <string>()); } else if (productValue.Type == JTokenType.Array) { productIDs = productValue.ToObject <List <string> >(); } // Build condition that matches all relevant productItems and get said productItems // The lookup groups the productItems and loanItems to a single product id. var condition = new MySqlConditionBuilder("product", MySqlDbType.String, productIDs.ToArray()); var productItemsArray = Connection.Select <ProductItem>(condition).ToArray(); var productItems = productItemsArray.ToLookup(x => x.ProductId); // Build condition that matches all relevand loanItems and get said loanItems condition = new MySqlConditionBuilder("product_item", MySqlDbType.Int32, productItemsArray.Select(x => x.Id.Value).Cast <object>().ToArray()); var loanItems = Connection.Select <LoanItem>(condition).ToLookup(x => productItemsArray.First(y => y.Id == x.ProductItem).ProductId); // DateTimeSpan representing now->midnight for filtering relevant loans DateTimeSpan today = new DateTimeSpan(DateTime.Now, DateTime.Now.Date.AddDays(1)); // Build response data var responseData = new JObject(); foreach (var productID in productIDs) { var items = productItems[productID]; var loans = loanItems[productID]; var relevantLoans = loans.Where(x => today.Overlaps(x.Start, x.End)).ToArray(); var entry = new JObject() { { "total", items.Count() }, { "totalReservations", loans.Count() }, { "reservations", relevantLoans.Length }, { "loanedOut", relevantLoans.Count(x => x.IsAcquired) }, { "inStock", items.Count() - relevantLoans.Count(x => x.IsAcquired) }, { "available", items.Count() - relevantLoans.Length } }; responseData.Add(productID, entry); } //Return response return(new JObject() { { "reason", null }, { "responseData", responseData } }); }
public bool Equals(DateTimeSpan span) => span.Start == Start && span.End == End;
public JObject getUnavailableDates(JObject request) { // Get arguments request.TryGetValue("productId", out JToken requestProductId); request.TryGetValue("start", out JToken requestStart); request.TryGetValue("end", out JToken requestEnd); // Verify arguments List <string> failedVerifications = new List <string>(); if (requestProductId == null || requestProductId.Type != JTokenType.String) { return(Templates.InvalidArgument("productId")); } if (requestStart == null) { return(Templates.InvalidArgument("start")); } if (requestEnd == null) { return(Templates.InvalidArgument("end")); } if (failedVerifications.Any()) { return(Templates.InvalidArguments(failedVerifications.ToArray())); } // Parse arguments DateTime start; DateTime end; try { start = requestStart == null ? new DateTime() : DateTime.Parse(requestStart.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'start'")); } try { end = requestEnd == null ? new DateTime() : DateTime.Parse(requestEnd.ToString()); } catch (Exception) { return(Templates.InvalidArgument("Unable to parse 'end'")); } var range = new DateTimeSpan(start, end); if (range.Duration.Days > 122) { return(Templates.InvalidArgument("start and end may not be more than 122 days apart.")); } // Get all items var condition = new MySqlConditionBuilder("product", MySqlDbType.String, requestProductId.ToString()); var productItems = Connection.Select <ProductItem>(condition).ToArray(); // Return empty response if no items were found if (!productItems.Any()) { return new JObject() { { "reason", null }, { "responseData", new JArray() } } } ; // Get all loans within the specified range condition = new MySqlConditionBuilder("product_item", MySqlDbType.Int32, productItems.Select(x => x.Id).Cast <object>().ToArray()); condition.And() .Column("end") .GreaterThanOrEqual() .Operand(range.Start, MySqlDbType.DateTime); condition.And() .Column("start") .LessThanOrEqual() .Operand(range.End, MySqlDbType.DateTime); var loanItems = Connection.Select <LoanItem>(condition).ToArray(); // No idea if this is the most efficient way, but it basically iterates // through all dates of a loan and slaps them in a cache. If the cache // already contains the date, it increases the counter. // If the counter is larger or equal to the total amount of items, we // consider that date unavailable. var dateCache = new Dictionary <DateTime, int>(); foreach (var loan in loanItems) { while (loan.Start < loan.End) { if (loan.Start < range.Start) { loan.Start = loan.Start.AddDays(1); continue; } if (loan.End > range.End) { break; } if (dateCache.ContainsKey(loan.Start)) { dateCache[loan.Start]++; } else { dateCache[loan.Start] = 1; } loan.Start = loan.Start.AddDays(1); } } // Build base response var responseData = new JArray(); JObject response = new JObject() { { "reason", null }, { "responseData", responseData } }; // Add all dates with an amount of loans greater or equal to the max available items foreach ((DateTime date, int count) in dateCache) { if (count >= productItems.Length) { responseData.Add((long)(date.ToUniversalTime() - Epoch).TotalMilliseconds); } } return(response); } }