using System.Globalization; using System.Text; using System.Text.Json; using Tiku.Domain.Learning; namespace Tiku.Application.Learning; public sealed record QuestionGradingInput( string QuestionType, int? CorrectOptionIndex, JsonElement CorrectOptionIndices, string? CorrectAnswerText, JsonElement GradingRules, IReadOnlyCollection? SelectedOptionIndices, string? AnswerText, decimal Score); public sealed record QuestionGradingResult( AnswerGradingStatus Status, bool? IsCorrect, decimal? AwardedScore); public static class QuestionGrader { private static readonly IReadOnlySet SubjectiveTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { "short_answer", "reading", "programming" }; public static QuestionGradingResult Grade(QuestionGradingInput input) { var type = input.QuestionType.Trim().ToLowerInvariant(); if (SubjectiveTypes.Contains(type)) return new QuestionGradingResult(AnswerGradingStatus.PendingReview, null, null); var correct = type switch { "choice" => GradeChoice(input), "multiple_choice" => GradeMultipleChoice(input), "true_false" => GradeTrueFalse(input), "fill_blank" => GradeFillBlank(input), _ => throw new InvalidOperationException($"Unsupported question type '{input.QuestionType}'.") }; return new QuestionGradingResult( correct ? AnswerGradingStatus.Correct : AnswerGradingStatus.Incorrect, correct, correct ? input.Score : 0); } public static bool HasValidAuthoritativeAnswer( string questionType, int? correctOptionIndex, JsonElement correctOptionIndices, string? answerText) { return questionType.Trim().ToLowerInvariant() switch { "choice" => correctOptionIndex is >= 0, "multiple_choice" => ReadIndices(correctOptionIndices).Count > 0, "true_false" => TryNormalizeBoolean(answerText, out _), "fill_blank" => !string.IsNullOrWhiteSpace(answerText), "short_answer" or "reading" or "programming" => true, _ => false }; } private static bool GradeChoice(QuestionGradingInput input) { var selected = input.SelectedOptionIndices?.Distinct().ToArray() ?? []; return input.CorrectOptionIndex.HasValue && selected.Length == 1 && selected[0] == input.CorrectOptionIndex.Value; } private static bool GradeMultipleChoice(QuestionGradingInput input) { var expected = ReadIndices(input.CorrectOptionIndices); var selected = (input.SelectedOptionIndices ?? []).Distinct().Order().ToArray(); return expected.Count > 0 && expected.SequenceEqual(selected); } private static bool GradeTrueFalse(QuestionGradingInput input) { return TryNormalizeBoolean(input.CorrectAnswerText, out var expected) && TryNormalizeBoolean(input.AnswerText, out var actual) && expected == actual; } private static bool GradeFillBlank(QuestionGradingInput input) { var accepted = new HashSet(StringComparer.Ordinal); AddNormalized(accepted, input.CorrectAnswerText); if (input.GradingRules.ValueKind == JsonValueKind.Object && input.GradingRules.TryGetProperty("acceptedAnswers", out var alternatives) && alternatives.ValueKind == JsonValueKind.Array) foreach (var alternative in alternatives.EnumerateArray()) if (alternative.ValueKind == JsonValueKind.String) AddNormalized(accepted, alternative.GetString()); return accepted.Count > 0 && accepted.Contains(NormalizeText(input.AnswerText)); } private static IReadOnlyList ReadIndices(JsonElement value) { if (value.ValueKind != JsonValueKind.Array) return []; return value.EnumerateArray() .Where(item => item.TryGetInt32(out _)) .Select(item => item.GetInt32()) .Distinct() .Order() .ToArray(); } private static bool TryNormalizeBoolean(string? value, out bool result) { switch (NormalizeText(value)) { case "true" or "1" or "yes" or "正确" or "对": result = true; return true; case "false" or "0" or "no" or "错误" or "错": result = false; return true; default: result = false; return false; } } private static void AddNormalized(ISet values, string? value) { var normalized = NormalizeText(value); if (normalized.Length > 0) values.Add(normalized); } private static string NormalizeText(string? value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; var normalized = value.Normalize(NormalizationForm.FormKC).Trim().ToLower(CultureInfo.InvariantCulture); var builder = new StringBuilder(normalized.Length); var previousWhitespace = false; foreach (var character in normalized) if (char.IsWhiteSpace(character)) { if (!previousWhitespace) { builder.Append(' '); previousWhitespace = true; } } else { builder.Append(character); previousWhitespace = false; } return builder.ToString(); } }