556 lines
23 KiB
C#
556 lines
23 KiB
C#
using MediatR;
|
|
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
|
|
using cuqmbr.TravelGuide.Application.Common.Exceptions;
|
|
using cuqmbr.TravelGuide.Domain.Entities;
|
|
using AutoMapper;
|
|
using QuikGraph;
|
|
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
|
|
using cuqmbr.TravelGuide.Domain.Enums;
|
|
using cuqmbr.TravelGuide.Application.Common.Extensions;
|
|
|
|
namespace cuqmbr.TravelGuide.Application
|
|
.VehicleEnrollmentSearch.Queries.SearchAll;
|
|
|
|
// TODO: Add configurable time between transfers.
|
|
// TODO: Refactor DTO creation code to use mapper as much as possible.
|
|
public class SearchAllQueryHandler :
|
|
IRequestHandler<SearchAllQuery, IEnumerable<VehicleEnrollmentSearchDto>>
|
|
{
|
|
private readonly UnitOfWork _unitOfWork;
|
|
private readonly IMapper _mapper;
|
|
|
|
private readonly SessionCurrencyService _sessionCurrencyService;
|
|
private readonly CurrencyConverterService _currencyConverterService;
|
|
|
|
private readonly SessionTimeZoneService _sessionTimeZoneService;
|
|
|
|
public SearchAllQueryHandler(
|
|
UnitOfWork unitOfWork,
|
|
IMapper mapper,
|
|
SessionCurrencyService sessionCurrencyService,
|
|
CurrencyConverterService currencyConverterService,
|
|
SessionTimeZoneService sessionTimeZoneService)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_mapper = mapper;
|
|
_sessionCurrencyService = sessionCurrencyService;
|
|
_currencyConverterService = currencyConverterService;
|
|
_sessionTimeZoneService = sessionTimeZoneService;
|
|
}
|
|
|
|
public async Task<IEnumerable<VehicleEnrollmentSearchDto>> Handle(
|
|
SearchAllQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Get related data
|
|
|
|
var zeroTime = TimeOnly.FromTimeSpan(TimeSpan.Zero);
|
|
var departureDate =
|
|
new DateTimeOffset(request.DepartureDate, zeroTime, TimeSpan.Zero);
|
|
|
|
var range = TimeSpan.FromDays(3);
|
|
var vehicleEnrollments = (await _unitOfWork.VehicleEnrollmentRepository
|
|
.GetPageAsync(
|
|
e =>
|
|
e.DepartureTime >= departureDate.Subtract(range) &&
|
|
e.DepartureTime <= departureDate.Add(range) &&
|
|
request.VehicleTypes.Contains(e.Vehicle.VehicleType),
|
|
e => e.Route,
|
|
1, int.MaxValue, cancellationToken))
|
|
.Items;
|
|
|
|
if (vehicleEnrollments.Count == 0)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var vehicleEnrollmentIds = vehicleEnrollments.Select(e => e.Id);
|
|
var routeAddressDetails = (await _unitOfWork.RouteAddressDetailRepository
|
|
.GetPageAsync(
|
|
e => vehicleEnrollmentIds.Contains(e.VehicleEnrollmentId),
|
|
e => e.RouteAddress.Address.City.Region.Country,
|
|
1, int.MaxValue, cancellationToken))
|
|
.Items;
|
|
|
|
var vehicles = (await _unitOfWork.VehicleRepository
|
|
.GetPageAsync(
|
|
e => e.Enrollments.All(e => vehicleEnrollmentIds.Contains(e.Id)),
|
|
1, int.MaxValue, cancellationToken))
|
|
.Items;
|
|
|
|
var companyIds = vehicles.Select(e => e.CompanyId);
|
|
var companies = (await _unitOfWork.CompanyRepository
|
|
.GetPageAsync(
|
|
e => companyIds.Contains(e.Id),
|
|
1, int.MaxValue, cancellationToken))
|
|
.Items;
|
|
|
|
|
|
// Hydrate vehicle enrollments with:
|
|
// - route address details;
|
|
// - vehicle info;
|
|
// - comapny info.
|
|
|
|
foreach (var vehicleEnrollment in vehicleEnrollments)
|
|
{
|
|
vehicleEnrollment.RouteAddressDetails = routeAddressDetails
|
|
.Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id)
|
|
.OrderBy(e => e.RouteAddress.Order)
|
|
.ToArray();
|
|
|
|
vehicleEnrollment.Vehicle = vehicles
|
|
.Single(e => e.Id == vehicleEnrollment.VehicleId);
|
|
|
|
vehicleEnrollment.Vehicle.Company = companies
|
|
.Single(e => e.Id == vehicleEnrollment.Vehicle.CompanyId);
|
|
}
|
|
|
|
|
|
// Creat and fill graph data structure
|
|
|
|
var graph = new AdjacencyGraph<
|
|
Address, TaggedEdge<Address, RouteAddressDetail>>();
|
|
|
|
foreach (var vehicleEnrollment in vehicleEnrollments)
|
|
{
|
|
var vehicleEnrollmentRouteAddressDetails = routeAddressDetails
|
|
.Where(e => e.VehicleEnrollmentId == vehicleEnrollment.Id)
|
|
.OrderBy(e => e.RouteAddress.Order);
|
|
|
|
for (int i = 1; i < vehicleEnrollmentRouteAddressDetails.Count(); i++)
|
|
{
|
|
var sourceRouteAddressDetail =
|
|
vehicleEnrollmentRouteAddressDetails.ElementAt(i - 1);
|
|
var targetRouteAddressDetail =
|
|
vehicleEnrollmentRouteAddressDetails.ElementAt(i);
|
|
|
|
var sourceAddress =
|
|
sourceRouteAddressDetail.RouteAddress.Address;
|
|
var targetAddress =
|
|
targetRouteAddressDetail.RouteAddress.Address;
|
|
|
|
var weight = sourceRouteAddressDetail.CostToNextAddress;
|
|
|
|
graph.AddVerticesAndEdge(
|
|
new TaggedEdge<Address, RouteAddressDetail>(
|
|
sourceAddress, targetAddress, sourceRouteAddressDetail));
|
|
}
|
|
}
|
|
|
|
|
|
// Find paths
|
|
|
|
var departureAddress = routeAddressDetails
|
|
.First(e => e.RouteAddress.Address.Guid == request.DepartureAddressGuid)
|
|
.RouteAddress.Address;
|
|
var arrivalAddress = routeAddressDetails
|
|
.First(e => e.RouteAddress.Address.Guid == request.ArrivalAddressGuid)
|
|
.RouteAddress.Address;
|
|
|
|
|
|
var paths = new List<List<TaggedEdge<Address, RouteAddressDetail>>>();
|
|
var queue = new Queue<(TaggedEdge<Address, RouteAddressDetail> edge, List<TaggedEdge<Address, RouteAddressDetail>> path)>();
|
|
|
|
foreach (var edge in graph.OutEdges(departureAddress))
|
|
{
|
|
queue.Enqueue((edge, new List<TaggedEdge<Address, RouteAddressDetail>>() { edge }));
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
|
|
if (current.edge.Target.Equals(arrivalAddress))
|
|
{
|
|
paths.Add(current.path);
|
|
continue;
|
|
}
|
|
|
|
foreach (var edge in graph.OutEdges(current.edge.Target))
|
|
{
|
|
var neighbor = edge;
|
|
if (!current.path.Contains(neighbor))
|
|
{
|
|
var newPath = new List<TaggedEdge<Address, RouteAddressDetail>>(current.path) { neighbor };
|
|
queue.Enqueue((neighbor, newPath));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Create DTO object
|
|
|
|
var result = new List<VehicleEnrollmentSearchDto>();
|
|
|
|
foreach (var path in paths)
|
|
{
|
|
var vehicleEnrollmentDtos =
|
|
new List<VehicleEnrollmentSearchVehicleEnrollmentDto>();
|
|
|
|
var addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
|
|
|
|
var firstRouteAddressId = path.First().Tag.RouteAddressId;
|
|
|
|
Guid lastRouteAddressGuid;
|
|
long lastRouteAddressId;
|
|
|
|
decimal vehicleEnrollmentCost;
|
|
Currency vehicleEnrollmentCurrency;
|
|
|
|
decimal costToNextAddress;
|
|
|
|
short addressOrder = 1;
|
|
short enrollmentOrder = 1;
|
|
|
|
Address source;
|
|
Address nextSource;
|
|
|
|
Address target;
|
|
Address nextTarget;
|
|
|
|
RouteAddressDetail tag;
|
|
RouteAddressDetail nextTag;
|
|
|
|
for (int i = 0; i < path.Count - 1; i++)
|
|
{
|
|
var edge = path[i];
|
|
var nextEdge = path[i+1];
|
|
|
|
source = edge.Source;
|
|
nextSource = nextEdge.Source;
|
|
|
|
tag = edge.Tag;
|
|
nextTag = nextEdge.Tag;
|
|
|
|
|
|
costToNextAddress = await _currencyConverterService
|
|
.ConvertAsync(tag.CostToNextAddress,
|
|
tag.VehicleEnrollment.Currency,
|
|
_sessionCurrencyService.Currency, cancellationToken);
|
|
|
|
addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
|
|
{
|
|
Uuid = source.Guid,
|
|
Name = source.Name,
|
|
Longitude = source.Latitude,
|
|
Latitude = source.Longitude,
|
|
CountryUuid = source.City.Region.Country.Guid,
|
|
CountryName = source.City.Region.Country.Name,
|
|
RegionUuid = source.City.Region.Guid,
|
|
RegionName = source.City.Region.Name,
|
|
CityUuid = source.City.Guid,
|
|
CityName = source.City.Name,
|
|
TimeToNextAddress = tag.TimeToNextAddress,
|
|
CostToNextAddress = costToNextAddress,
|
|
CurrentAddressStopTime = tag.CurrentAddressStopTime,
|
|
Order = addressOrder,
|
|
RouteAddressUuid = tag.RouteAddress.Guid
|
|
});
|
|
|
|
addressOrder++;
|
|
|
|
|
|
if (tag.VehicleEnrollmentId != nextTag.VehicleEnrollmentId)
|
|
{
|
|
target = edge.Target;
|
|
lastRouteAddressGuid = vehicleEnrollments
|
|
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
.RouteAddressDetails
|
|
.Select(e => e.RouteAddress)
|
|
.OrderBy(e => e.Order)
|
|
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
.Take(2)
|
|
.ElementAt(1)
|
|
.Guid;
|
|
|
|
addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
|
|
{
|
|
Uuid = target.Guid,
|
|
Name = target.Name,
|
|
Longitude = target.Latitude,
|
|
Latitude = target.Longitude,
|
|
CountryUuid = target.City.Region.Country.Guid,
|
|
CountryName = target.City.Region.Country.Name,
|
|
RegionUuid = target.City.Region.Guid,
|
|
RegionName = target.City.Region.Name,
|
|
CityUuid = target.City.Guid,
|
|
CityName = target.City.Name,
|
|
TimeToNextAddress = TimeSpan.Zero,
|
|
CostToNextAddress = 0,
|
|
CurrentAddressStopTime = TimeSpan.Zero,
|
|
Order = addressOrder,
|
|
RouteAddressUuid = lastRouteAddressGuid
|
|
});
|
|
|
|
|
|
lastRouteAddressId = vehicleEnrollments
|
|
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
.RouteAddressDetails
|
|
.Select(e => e.RouteAddress)
|
|
.OrderBy(e => e.Order)
|
|
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
.Take(2)
|
|
.ElementAt(1)
|
|
.Id;
|
|
|
|
vehicleEnrollmentCost = await _currencyConverterService
|
|
.ConvertAsync(
|
|
tag.VehicleEnrollment
|
|
.GetCost(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
tag.VehicleEnrollment.Currency,
|
|
_sessionCurrencyService.Currency,
|
|
cancellationToken);
|
|
|
|
vehicleEnrollmentCurrency =
|
|
_sessionCurrencyService.Currency.Equals(Currency.Default) ?
|
|
tag.VehicleEnrollment.Currency :
|
|
_sessionCurrencyService.Currency;
|
|
|
|
vehicleEnrollmentDtos.Add(
|
|
new VehicleEnrollmentSearchVehicleEnrollmentDto()
|
|
{
|
|
DepartureTime = tag.VehicleEnrollment
|
|
.GetDepartureTime(firstRouteAddressId)
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
ArrivalTime = tag.VehicleEnrollment
|
|
.GetArrivalTime(lastRouteAddressId)
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
TravelTime = tag.VehicleEnrollment
|
|
.GetTravelTime(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
TimeMoving = tag.VehicleEnrollment
|
|
.GetTimeMoving(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
TimeInStops = tag.VehicleEnrollment
|
|
.GetTimeInStops(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
NumberOfStops = tag.VehicleEnrollment
|
|
.GetNumberOfStops(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
Currency = vehicleEnrollmentCurrency.Name,
|
|
Cost = vehicleEnrollmentCost,
|
|
VehicleType = source.VehicleType.Name,
|
|
Uuid = tag.VehicleEnrollment.Guid,
|
|
Order = enrollmentOrder,
|
|
Addresses = addressDtos,
|
|
Company = _mapper
|
|
.Map<VehicleEnrollmentSearchCompanyDto>(
|
|
tag.VehicleEnrollment.Vehicle.Company),
|
|
Vehicle = _mapper
|
|
.Map<VehicleEnrollmentSearchVehicleDto>(
|
|
tag.VehicleEnrollment.Vehicle)
|
|
});
|
|
|
|
|
|
firstRouteAddressId = nextTag.RouteAddressId;
|
|
addressDtos = new List<VehicleEnrollmentSearchAddressDto>();
|
|
addressOrder = 1;
|
|
enrollmentOrder++;
|
|
}
|
|
}
|
|
|
|
// ---------------
|
|
|
|
source = path.Select(e => e.Source).Last();
|
|
target = path.Select(e => e.Target).Last();
|
|
tag = path.Select(e => e.Tag).Last();
|
|
|
|
|
|
lastRouteAddressGuid = vehicleEnrollments
|
|
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
.RouteAddressDetails
|
|
.Select(e => e.RouteAddress)
|
|
.OrderBy(e => e.Order)
|
|
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
.Take(2)
|
|
.ElementAt(1)
|
|
.Guid;
|
|
|
|
costToNextAddress = await _currencyConverterService
|
|
.ConvertAsync(tag.CostToNextAddress,
|
|
tag.VehicleEnrollment.Currency,
|
|
_sessionCurrencyService.Currency, cancellationToken);
|
|
|
|
addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
|
|
{
|
|
Uuid = source.Guid,
|
|
Name = source.Name,
|
|
Longitude = source.Latitude,
|
|
Latitude = source.Longitude,
|
|
CountryUuid = source.City.Region.Country.Guid,
|
|
CountryName = source.City.Region.Country.Name,
|
|
RegionUuid = source.City.Region.Guid,
|
|
RegionName = source.City.Region.Name,
|
|
CityUuid = source.City.Guid,
|
|
CityName = source.City.Name,
|
|
TimeToNextAddress = tag.TimeToNextAddress,
|
|
CostToNextAddress = 0,
|
|
CurrentAddressStopTime = tag.CurrentAddressStopTime,
|
|
Order = addressOrder,
|
|
RouteAddressUuid = lastRouteAddressGuid
|
|
});
|
|
|
|
|
|
lastRouteAddressGuid = vehicleEnrollments
|
|
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
.RouteAddressDetails
|
|
.Select(e => e.RouteAddress)
|
|
.OrderBy(e => e.Order)
|
|
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
.Take(2)
|
|
.ElementAt(1)
|
|
.Guid;
|
|
|
|
addressDtos.Add(new VehicleEnrollmentSearchAddressDto()
|
|
{
|
|
Uuid = target.Guid,
|
|
Name = target.Name,
|
|
Longitude = target.Latitude,
|
|
Latitude = target.Longitude,
|
|
CountryUuid = target.City.Region.Country.Guid,
|
|
CountryName = target.City.Region.Country.Name,
|
|
RegionUuid = target.City.Region.Guid,
|
|
RegionName = target.City.Region.Name,
|
|
CityUuid = target.City.Guid,
|
|
CityName = target.City.Name,
|
|
TimeToNextAddress = TimeSpan.Zero,
|
|
CostToNextAddress = 0,
|
|
CurrentAddressStopTime = TimeSpan.Zero,
|
|
Order = addressOrder,
|
|
RouteAddressUuid = lastRouteAddressGuid
|
|
});
|
|
|
|
|
|
lastRouteAddressId = vehicleEnrollments
|
|
.Single(e => e.Id == tag.VehicleEnrollmentId)
|
|
.RouteAddressDetails
|
|
.Select(e => e.RouteAddress)
|
|
.OrderBy(e => e.Order)
|
|
.SkipWhile(e => e.Order != tag.RouteAddress.Order)
|
|
.Take(2)
|
|
.ElementAt(1)
|
|
.Id;
|
|
|
|
vehicleEnrollmentCost = await _currencyConverterService
|
|
.ConvertAsync(
|
|
tag.VehicleEnrollment
|
|
.GetCost(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
tag.VehicleEnrollment.Currency,
|
|
_sessionCurrencyService.Currency,
|
|
cancellationToken);
|
|
|
|
vehicleEnrollmentCurrency =
|
|
_sessionCurrencyService.Currency.Equals(Currency.Default) ?
|
|
tag.VehicleEnrollment.Currency :
|
|
_sessionCurrencyService.Currency;
|
|
|
|
vehicleEnrollmentDtos.Add(
|
|
new VehicleEnrollmentSearchVehicleEnrollmentDto()
|
|
{
|
|
DepartureTime = tag.VehicleEnrollment
|
|
.GetDepartureTime(firstRouteAddressId)
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
ArrivalTime = tag.VehicleEnrollment
|
|
.GetArrivalTime(lastRouteAddressId)
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
TravelTime = tag.VehicleEnrollment
|
|
.GetTravelTime(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
TimeMoving = tag.VehicleEnrollment
|
|
.GetTimeMoving(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
TimeInStops = tag.VehicleEnrollment
|
|
.GetTimeInStops(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
NumberOfStops = tag.VehicleEnrollment
|
|
.GetNumberOfStops(firstRouteAddressId,
|
|
lastRouteAddressId),
|
|
Currency = vehicleEnrollmentCurrency.Name,
|
|
Cost = vehicleEnrollmentCost,
|
|
VehicleType = source.VehicleType.Name,
|
|
Uuid = tag.VehicleEnrollment.Guid,
|
|
Order = enrollmentOrder,
|
|
Addresses = addressDtos,
|
|
Company = _mapper.Map<VehicleEnrollmentSearchCompanyDto>(
|
|
tag.VehicleEnrollment.Vehicle.Company),
|
|
Vehicle = _mapper.Map<VehicleEnrollmentSearchVehicleDto>(
|
|
tag.VehicleEnrollment.Vehicle)
|
|
});
|
|
|
|
// ---------------
|
|
|
|
|
|
var departureTime = vehicleEnrollmentDtos
|
|
.OrderBy(e => e.Order).First().DepartureTime;
|
|
var arrivalTime = vehicleEnrollmentDtos
|
|
.OrderBy(e => e.Order).Last().ArrivalTime;
|
|
var timeInStops = vehicleEnrollmentDtos
|
|
.Aggregate(TimeSpan.Zero, (sum, next) => sum += next.TimeInStops);
|
|
var numberOfTransfers = vehicleEnrollmentDtos.Count() - 1;
|
|
var cost = vehicleEnrollmentDtos
|
|
.Aggregate((decimal)0, (sum, next) => sum += next.Cost);
|
|
|
|
result.Add(new VehicleEnrollmentSearchDto()
|
|
{
|
|
DepartureTime = departureTime
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
ArrivalTime = arrivalTime
|
|
.ToOffset(_sessionTimeZoneService.TimeZone.BaseUtcOffset),
|
|
TravelTime = arrivalTime - departureTime,
|
|
TimeInStops = timeInStops,
|
|
NumberOfTransfers = numberOfTransfers,
|
|
Currency = _sessionCurrencyService.Currency.Name,
|
|
Cost = cost,
|
|
Enrollments = vehicleEnrollmentDtos
|
|
});
|
|
}
|
|
|
|
|
|
if (result.Count == 0)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
|
|
var filteredResult = result.Where(e =>
|
|
(request.TravelTimeGreaterThanOrEqualTo != null
|
|
? e.TravelTime >= request.TravelTimeGreaterThanOrEqualTo
|
|
: true) &&
|
|
(request.TravelTimeLessThanOrEqualTo != null
|
|
? e.TravelTime <= request.TravelTimeLessThanOrEqualTo
|
|
: true) &&
|
|
(request.CostGreaterThanOrEqualTo != null
|
|
? e.Cost >= request.CostGreaterThanOrEqualTo
|
|
: true) &&
|
|
(request.CostLessThanOrEqualTo != null
|
|
? e.Cost <= request.CostLessThanOrEqualTo
|
|
: true) &&
|
|
(request.NumberOfTransfersGreaterThanOrEqualTo != null
|
|
? e.NumberOfTransfers >= request.NumberOfTransfersGreaterThanOrEqualTo
|
|
: true) &&
|
|
(request.NumberOfTransfersLessThanOrEqualTo != null
|
|
? e.NumberOfTransfers <= request.NumberOfTransfersLessThanOrEqualTo
|
|
: true) &&
|
|
(request.DepartureTimeGreaterThanOrEqualTo != null
|
|
? e.DepartureTime >= request.DepartureTimeGreaterThanOrEqualTo
|
|
: true) &&
|
|
(request.DepartureTimeLessThanOrEqualTo != null
|
|
? e.DepartureTime <= request.DepartureTimeLessThanOrEqualTo
|
|
: true) &&
|
|
(request.ArrivalTimeGreaterThanOrEqualTo != null
|
|
? e.ArrivalTime >= request.ArrivalTimeGreaterThanOrEqualTo
|
|
: true) &&
|
|
(request.ArrivalTimeLessThanOrEqualTo != null
|
|
? e.ArrivalTime <= request.ArrivalTimeLessThanOrEqualTo
|
|
: true));
|
|
|
|
var sortedResult = QueryableExtension<VehicleEnrollmentSearchDto>
|
|
.ApplySort(filteredResult.AsQueryable(), request.Sort);
|
|
|
|
|
|
return sortedResult;
|
|
}
|
|
}
|