feat: add parsing from files
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Cronos;
|
using Cronos;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Mirea.Api.DataAccess.Persistence;
|
using Mirea.Api.DataAccess.Persistence;
|
||||||
@ -11,15 +13,19 @@ using Mirea.Api.Endpoint.Common.MapperDto;
|
|||||||
using Mirea.Api.Endpoint.Common.Services;
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model;
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
using Mirea.Api.Endpoint.Sync;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers.V1.Configuration;
|
namespace Mirea.Api.Endpoint.Controllers.V1.Configuration;
|
||||||
|
|
||||||
[ApiVersion("1.0")]
|
[ApiVersion("1.0")]
|
||||||
public class ScheduleController(ILogger<ScheduleController> logger, IOptionsSnapshot<GeneralConfig> config, UberDbContext dbContext) : ConfigurationBaseController
|
public class ScheduleController(ILogger<ScheduleController> logger, IOptionsSnapshot<GeneralConfig> config, UberDbContext dbContext, IServiceProvider provider) : ConfigurationBaseController
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the cron update schedule and calculates the next scheduled tasks based on the provided depth.
|
/// Retrieves the cron update schedule and calculates the next scheduled tasks based on the provided depth.
|
||||||
@ -113,7 +119,7 @@ public class ScheduleController(ILogger<ScheduleController> logger, IOptionsSnap
|
|||||||
return generalConfig.ScheduleSettings!.CronUpdateSkipDateList
|
return generalConfig.ScheduleSettings!.CronUpdateSkipDateList
|
||||||
.ConvertToDto();
|
.ConvertToDto();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the list of cron update skip dates in the configuration.
|
/// Updates the list of cron update skip dates in the configuration.
|
||||||
@ -139,4 +145,62 @@ public class ScheduleController(ILogger<ScheduleController> logger, IOptionsSnap
|
|||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads schedule files and initiates synchronization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files">The list of schedule files to upload.</param>
|
||||||
|
/// <param name="defaultCampus">The default campus for each uploaded file. Must match the number of files.</param>
|
||||||
|
/// <param name="force">If true, removes all existing lessons before synchronization. Default is false.</param>
|
||||||
|
/// <returns>Success or failure.</returns>
|
||||||
|
/// <exception cref="ControllerArgumentException">
|
||||||
|
/// Thrown if:
|
||||||
|
/// - No files are provided.
|
||||||
|
/// - The number of default campuses does not match the number of files.
|
||||||
|
/// - Any default campus is null or empty.
|
||||||
|
/// </exception>
|
||||||
|
[HttpPost("Upload")]
|
||||||
|
public async Task<ActionResult> UploadScheduleFiles(List<IFormFile>? files, [FromQuery] string[]? defaultCampus, [FromQuery] bool force = false)
|
||||||
|
{
|
||||||
|
if (files == null || files.Count == 0)
|
||||||
|
throw new ControllerArgumentException("No files were found.");
|
||||||
|
|
||||||
|
if (defaultCampus == null || files.Count != defaultCampus.Length)
|
||||||
|
throw new ControllerArgumentException("No default campuses are specified for the file.");
|
||||||
|
|
||||||
|
if (defaultCampus.Any(string.IsNullOrEmpty))
|
||||||
|
throw new ControllerArgumentException("Each file should have a default campus.");
|
||||||
|
|
||||||
|
var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
|
||||||
|
|
||||||
|
if (!Directory.Exists(tempDirectory))
|
||||||
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
|
||||||
|
List<(string, string)> filePaths = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < files.Count; i++)
|
||||||
|
{
|
||||||
|
if (files[i].Length <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var filePath = Path.Combine(tempDirectory, files[i].FileName);
|
||||||
|
|
||||||
|
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||||
|
await files[i].CopyToAsync(stream);
|
||||||
|
|
||||||
|
filePaths.Add((filePath, defaultCampus[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
var sync = (ScheduleSynchronizer)ActivatorUtilities.GetServiceOrCreateInstance(provider, typeof(ScheduleSynchronizer));
|
||||||
|
|
||||||
|
if (force)
|
||||||
|
{
|
||||||
|
dbContext.Lessons.RemoveRange(dbContext.Lessons.ToList());
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = sync.StartSync(filePaths, CancellationToken.None);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,9 +5,9 @@
|
|||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Company>Winsomnia</Company>
|
<Company>Winsomnia</Company>
|
||||||
<Version>1.0-rc6</Version>
|
<Version>1.0-rc7</Version>
|
||||||
<AssemblyVersion>1.0.2.6</AssemblyVersion>
|
<AssemblyVersion>1.0.2.7</AssemblyVersion>
|
||||||
<FileVersion>1.0.2.6</FileVersion>
|
<FileVersion>1.0.2.7</FileVersion>
|
||||||
<AssemblyName>Mirea.Api.Endpoint</AssemblyName>
|
<AssemblyName>Mirea.Api.Endpoint</AssemblyName>
|
||||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@ -41,6 +41,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.3.1" />
|
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.3.1" />
|
||||||
|
<PackageReference Include="Mirea.Tools.Schedule.Parser" Version="1.2.5" />
|
||||||
<PackageReference Include="Mirea.Tools.Schedule.WebParser" Version="1.0.6" />
|
<PackageReference Include="Mirea.Tools.Schedule.WebParser" Version="1.0.6" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
|
@ -6,9 +6,11 @@ using Mirea.Api.DataAccess.Persistence;
|
|||||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
using Mirea.Api.Endpoint.Configuration.Model;
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
using Mirea.Api.Endpoint.Sync.Common;
|
using Mirea.Api.Endpoint.Sync.Common;
|
||||||
|
using Mirea.Tools.Schedule.Parser.Domain;
|
||||||
using Mirea.Tools.Schedule.WebParser;
|
using Mirea.Tools.Schedule.WebParser;
|
||||||
using Mirea.Tools.Schedule.WebParser.Common.Domain;
|
using Mirea.Tools.Schedule.WebParser.Common.Domain;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -220,35 +222,15 @@ internal partial class ScheduleSynchronizer(UberDbContext dbContext, IOptionsSna
|
|||||||
await dbContext.LessonAssociations.BulkSynchronizeAsync(_lessonAssociation.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
await dbContext.LessonAssociations.BulkSynchronizeAsync(_lessonAssociation.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartSync(CancellationToken cancellationToken)
|
private async Task Sync(Func<CancellationToken, Task<List<GroupResult>>> parseDataAsync, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var pairPeriods = config.Value.ScheduleSettings?.PairPeriod;
|
|
||||||
var startTerm = config.Value.ScheduleSettings?.StartTerm;
|
|
||||||
|
|
||||||
if (pairPeriods == null || startTerm == null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("It is not possible to synchronize the schedule due to the fact that the {Arg1} or {Arg2} variable is not initialized.",
|
|
||||||
nameof(pairPeriods),
|
|
||||||
nameof(startTerm));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stopwatch watch = new();
|
Stopwatch watch = new();
|
||||||
watch.Start();
|
watch.Start();
|
||||||
|
|
||||||
var parser = new Parser
|
|
||||||
{
|
|
||||||
Pairs = pairPeriods
|
|
||||||
.ToDictionary(x => x.Key,
|
|
||||||
x => (x.Value.Start, x.Value.End)),
|
|
||||||
TermStart = startTerm.Value.ToDateTime(new TimeOnly(0, 0, 0))
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogDebug("Start parsing schedule");
|
logger.LogDebug("Start parsing schedule");
|
||||||
var data = await parser.ParseAsync(cancellationToken);
|
var data = await parseDataAsync(cancellationToken);
|
||||||
|
|
||||||
watch.Stop();
|
watch.Stop();
|
||||||
var parsingTime = watch.ElapsedMilliseconds;
|
var parsingTime = watch.ElapsedMilliseconds;
|
||||||
@ -282,11 +264,255 @@ internal partial class ScheduleSynchronizer(UberDbContext dbContext, IOptionsSna
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred during synchronization.");
|
logger.LogError(ex, "An error occurred during synchronization.");
|
||||||
maintenanceMode.DisableMaintenanceMode();
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
maintenanceMode.DisableMaintenanceMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartSync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pairPeriods = config.Value.ScheduleSettings?.PairPeriod
|
||||||
|
.ToDictionary(x => x.Key, x => (x.Value.Start, x.Value.End));
|
||||||
|
|
||||||
|
var startTerm = config.Value.ScheduleSettings?.StartTerm;
|
||||||
|
|
||||||
|
if (pairPeriods == null || startTerm == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("It is not possible to synchronize the schedule due to the fact that the {Arg1} or {Arg2} variable is not initialized.",
|
||||||
|
nameof(pairPeriods),
|
||||||
|
nameof(startTerm));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parser = new Parser
|
||||||
|
{
|
||||||
|
Pairs = pairPeriods
|
||||||
|
.ToDictionary(x => x.Key,
|
||||||
|
x => (x.Value.Start, x.Value.End)),
|
||||||
|
TermStart = startTerm.Value.ToDateTime(new TimeOnly(0, 0, 0))
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Sync(parser.ParseAsync, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred during synchronization.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
maintenanceMode.DisableMaintenanceMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartSync(List<(string File, string Campus)> files, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var pairPeriods = config.Value.ScheduleSettings?.PairPeriod
|
||||||
|
.ToDictionary(x => x.Key, x => (x.Value.Start, x.Value.End));
|
||||||
|
|
||||||
|
if (pairPeriods == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("It is not possible to synchronize the schedule due to the fact that the {Arg1} variable is not initialized.",
|
||||||
|
nameof(pairPeriods));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task<List<GroupResult>> ParseTask(CancellationToken ctx)
|
||||||
|
{
|
||||||
|
var mappedData = new ConcurrentBag<GroupResult>();
|
||||||
|
|
||||||
|
ParallelOptions options = new() { CancellationToken = ctx, MaxDegreeOfParallelism = Environment.ProcessorCount };
|
||||||
|
Parallel.ForEach(files, options, (file) =>
|
||||||
|
{
|
||||||
|
var parser = new Tools.Schedule.Parser.Parser();
|
||||||
|
var result = ConvertToGroupResults(parser.Parse(file.File, pairPeriods), file.Campus);
|
||||||
|
|
||||||
|
foreach (var item in result) mappedData.Add(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(mappedData.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Sync(ParseTask, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred during synchronization.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
maintenanceMode.DisableMaintenanceMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<GroupResult> ConvertToGroupResults(IEnumerable<GroupInfo> groups, string campusDefault, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = new List<GroupResult>();
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
foreach (var day in group.Days)
|
||||||
|
{
|
||||||
|
foreach (var pair in day.Lessons)
|
||||||
|
{
|
||||||
|
foreach (var lesson in pair.Value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(lesson.TypeOfOccupation))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (weeks, isExclude) = ParseWeeks(lesson.Discipline);
|
||||||
|
|
||||||
|
var (lectureHalls, campuses) = ParseLectureHalls(lesson.LectureHall, campusDefault);
|
||||||
|
|
||||||
|
var groupResult = new GroupResult
|
||||||
|
{
|
||||||
|
Day = day.DayOfWeek,
|
||||||
|
Pair = pair.Key,
|
||||||
|
IsEven = lesson.IsEven,
|
||||||
|
Group = group.GroupName,
|
||||||
|
Discipline = NormalizeDiscipline(lesson.Discipline),
|
||||||
|
Professor = ParseProfessors(lesson.Professor),
|
||||||
|
TypeOfOccupation = lesson.TypeOfOccupation,
|
||||||
|
LectureHalls = lectureHalls,
|
||||||
|
Campuses = campuses,
|
||||||
|
SpecialWeek = weeks,
|
||||||
|
IsExclude = isExclude
|
||||||
|
};
|
||||||
|
|
||||||
|
result.Add(groupResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[]? ParseProfessors(string? input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input)) return null;
|
||||||
|
|
||||||
|
var normalized = Regex.Replace(input
|
||||||
|
.Replace("\n", " ")
|
||||||
|
.Replace(",", " "),
|
||||||
|
@"\s+", " ").Trim();
|
||||||
|
|
||||||
|
return ProfessorFullName().Matches(normalized)
|
||||||
|
.Select(m => $"{m.Groups["surname"].Value} {m.Groups["initials"].Value}".Trim())
|
||||||
|
.Where(x => !string.IsNullOrEmpty(x))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int[]? weeks, bool? isExclude) ParseWeeks(string discipline)
|
||||||
|
{
|
||||||
|
var match = ParseSpecificWeeks().Match(discipline);
|
||||||
|
|
||||||
|
if (!match.Success) return (null, null);
|
||||||
|
|
||||||
|
var numbers = new List<int>();
|
||||||
|
var ranges = match.Groups[2].Value.Split(',');
|
||||||
|
|
||||||
|
foreach (var range in ranges)
|
||||||
|
{
|
||||||
|
if (range.Contains('-'))
|
||||||
|
{
|
||||||
|
var parts = range.Split('-');
|
||||||
|
if (int.TryParse(parts[0], out var start) &&
|
||||||
|
int.TryParse(parts[1], out var end))
|
||||||
|
{
|
||||||
|
numbers.AddRange(Enumerable.Range(start, end - start + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (int.TryParse(range, out var num)) numbers.Add(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
weeks: numbers.Distinct().OrderBy(x => x).ToArray(),
|
||||||
|
isExclude: match.Groups[1].Success
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeDiscipline(string input)
|
||||||
|
{
|
||||||
|
var normalized = Regex.Replace(input
|
||||||
|
.Replace("\n", " ")
|
||||||
|
.Replace("\r", " "),
|
||||||
|
@"\s{2,}", " ");
|
||||||
|
|
||||||
|
normalized = Regex.Replace(normalized,
|
||||||
|
@"(\S+)\s(\S{3,})",
|
||||||
|
"$1 $2");
|
||||||
|
|
||||||
|
normalized = ParseSpecificWeeks().Replace(normalized, "");
|
||||||
|
|
||||||
|
return normalized.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string[]? lectureHalls, string[]? campuses) ParseLectureHalls(string? input, string defaultCampus)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return (null, null);
|
||||||
|
|
||||||
|
var matches = ParseLectureCampus().Matches(input);
|
||||||
|
var lectureHalls = new List<string>();
|
||||||
|
var campuses = new List<string>();
|
||||||
|
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
if (match.Groups["lectureWithCampus"].Success)
|
||||||
|
{
|
||||||
|
var raw = match.Value.Split('(');
|
||||||
|
var campus = raw.LastOrDefault()?.Trim(')').Trim();
|
||||||
|
var lecture = raw.FirstOrDefault()?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(campus) || string.IsNullOrEmpty(lecture))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
campuses.Add(campus);
|
||||||
|
lectureHalls.Add(lecture);
|
||||||
|
}
|
||||||
|
else if (match.Groups["lecture"].Success)
|
||||||
|
{
|
||||||
|
var lecture = match.Value.Trim();
|
||||||
|
if (string.IsNullOrEmpty(lecture))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
campuses.Add(defaultCampus);
|
||||||
|
lectureHalls.Add(lecture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
lectureHalls: lectureHalls.ToArray(),
|
||||||
|
campuses: campuses.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[GeneratedRegex(@"\w{4}-\d{2}-\d{2}(?=\s?\d?\s?[Пп]/?[Гг]\s?\d?)?")]
|
[GeneratedRegex(@"\w{4}-\d{2}-\d{2}(?=\s?\d?\s?[Пп]/?[Гг]\s?\d?)?")]
|
||||||
private static partial Regex OnlyGroupName();
|
private static partial Regex OnlyGroupName();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?<surname>[А-ЯЁ][а-яё]+(-[А-ЯЁ][а-яё]+)?)\s*(?<initials>[А-ЯЁ]\.[А-ЯЁ]?\.?)?", RegexOptions.IgnorePatternWhitespace)]
|
||||||
|
private static partial Regex ProfessorFullName();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"([Кк]р\.?)?\s*((\d+-\d+|\d+)(,\s*\d+(-\d+)?)*)\s*[Нн]\.?", RegexOptions.IgnoreCase, "ru-RU")]
|
||||||
|
private static partial Regex ParseSpecificWeeks();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?<lectureWithCampus>[^,.\n]+\s?\([А-Яа-яA-Za-z]+-?\d+\))|(?<lecture>[^,.\n]+)")]
|
||||||
|
private static partial Regex ParseLectureCampus();
|
||||||
}
|
}
|
Reference in New Issue
Block a user