diff --git a/app/main/checks/presentation_checks/excessive_decimal_places.py b/app/main/checks/presentation_checks/excessive_decimal_places.py new file mode 100644 index 00000000..87bb769b --- /dev/null +++ b/app/main/checks/presentation_checks/excessive_decimal_places.py @@ -0,0 +1,49 @@ +import re +from ..base_check import BasePresCriterion, answer +from utils.decimal_checker import DecimalPlacesChecker, DocumentType + + +class PresExcessiveDecimalPlacesCheck(BasePresCriterion): + description = 'Проверка на избыточное количество десятичных знаков в числах' + label = "Проверка на избыточное количество десятичных знаков" + id = 'pres_excessive_decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=1): + super().__init__(file_info) + self.max_decimal_places = max_decimal_places + self.max_number_of_violations = max_number_of_violations + self.checker = DecimalPlacesChecker(max_decimal_places, DocumentType.PRESENTATION) + + def check(self): + + detected_slides = {} + total_violations = 0 + + for slide_num, slide_text in enumerate(self.file.get_text_from_slides()): + lines = re.split(r'\n', slide_text) + + slide_violations = self.checker.find_violations_in_lines(lines) + + for line_idx, number_str, decimal_places, line in slide_violations: + total_violations += 1 + + if slide_num not in detected_slides: + detected_slides[slide_num] = [] + + violation_msg = self.checker.format_violation_message( + line_idx, line, number_str, decimal_places + ) + detected_slides[slide_num].append(violation_msg) + + if total_violations > self.max_number_of_violations: + result_str = self.checker.format_failure_message( + total_violations, + detected_slides, + format_page_link_fn=self.format_page_link + ) + result_score = 0 + else: + result_str = self.checker.format_success_message() + result_score = 1 + + return answer(result_score, result_str) diff --git a/app/main/checks/report_checks/excessive_decimal_places_check.py b/app/main/checks/report_checks/excessive_decimal_places_check.py new file mode 100644 index 00000000..796f2952 --- /dev/null +++ b/app/main/checks/report_checks/excessive_decimal_places_check.py @@ -0,0 +1,51 @@ +import re +from ..base_check import BaseReportCriterion, answer +from utils.decimal_checker import DecimalPlacesChecker + + +class ReportExcessiveDecimalPlacesCheck(BaseReportCriterion): + label = "Проверка на избыточное количество десятичных знаков" + description = 'Проверка на избыточное количество десятичных знаков в числах' + id = 'excessive_decimal_places_check' + + def __init__(self, file_info, max_decimal_places=2, max_number_of_violations=2): + super().__init__(file_info) + self.max_decimal_places = max_decimal_places + self.max_number_of_violations = max_number_of_violations + self.checker = DecimalPlacesChecker(max_decimal_places) + + def check(self): + if self.file.page_counter() < 4: + return answer(False, "В отчете недостаточно страниц. Нечего проверять.") + + detected_pages = {} + total_violations = 0 + + for page_num, page_text in self.file.pdf_file.get_text_on_page().items(): + lines = re.split(r'\n', page_text) + + page_violations = self.checker.find_violations_in_lines(lines) + + for line_idx, number_str, decimal_places, line in page_violations: + total_violations += 1 + + if page_num not in detected_pages: + detected_pages[page_num] = [] + + violation_msg = self.checker.format_violation_message( + line_idx, line, number_str, decimal_places + ) + detected_pages[page_num].append(violation_msg) + + if total_violations > self.max_number_of_violations: + result_str = self.checker.format_failure_message( + total_violations, + detected_pages, + format_page_link_fn=self.format_page_link + ) + result_score = 0 + else: + result_str = self.checker.format_success_message() + result_score = 1.0 + + return answer(result_score, result_str) diff --git a/app/utils/decimal_checker.py b/app/utils/decimal_checker.py new file mode 100644 index 00000000..47d9dd13 --- /dev/null +++ b/app/utils/decimal_checker.py @@ -0,0 +1,126 @@ +import re +from enum import Enum + +class DocumentType(Enum): + REPORT = 'report' + PRESENTATION = 'pres' + +class DecimalPlacesChecker: + """ + Класс для проверки чисел на избыточное количество десятичных знаков. + Игнорирует IP-адреса, версии ПО и другие составные числа. + + Проверяет: + - Обычные числа с десятичными знаками (например, -3.14159) + Не проверяет: + - IP-адреса (например, 192.168.1.1) + - Версии ПО (например, 1.2.3.4) + - Другие составные числа + """ + + DECIMAL_PATTERN = r'(? 0 else ' ' + + if char_before == '.': + return False + + if end_pos < len(text): + char_after = text[end_pos] + if char_after == '.' and end_pos + 1 < len(text) and text[end_pos + 1].isdigit(): + return False + + return True + + def count_decimal_places(self, number_str): + normalized = number_str.replace(',', '.') + + if '.' in normalized: + decimal_part = normalized.split('.')[1] + return len(decimal_part) + + return 0 + + def has_excessive_decimals(self, number_str): + return self.count_decimal_places(number_str) > self.max_decimal_places + + def find_violations_in_text(self, text): + violations = [] + matches = re.finditer(self.DECIMAL_PATTERN, text) + + for match in matches: + if not self.is_valid_number(match, text): + continue + + number_str = match.group() + decimal_places = self.count_decimal_places(number_str) + + if decimal_places > self.max_decimal_places: + violations.append((number_str, decimal_places, match)) + + return violations + + def find_violations_in_lines(self, lines): + violations = [] + + for line_idx, line in enumerate(lines): + line_violations = self.find_violations_in_text(line) + + for number_str, decimal_places, match in line_violations: + violations.append((line_idx, number_str, decimal_places, line)) + + return violations + + def highlight_number(self, line, number_str): + return line.replace(number_str, f'{number_str}', 1) + + def format_violation_message(self, line_idx, line, number_str, decimal_places): + highlighted_line = self.highlight_number(line, number_str) + return ( + f'Строка {line_idx + 1}: {highlighted_line} ' + f'(найдено {decimal_places} знаков после запятой, ' + f'максимум: {self.max_decimal_places})' + ) + + def format_success_message(self): + if self.type == DocumentType.REPORT: + document_type = "документе" + elif self.type == DocumentType.PRESENTATION: + document_type = "презентации" + return ( + f'Проверка пройдена! Все числа в {document_type} имеют допустимое количество ' + f'десятичных знаков (не более {self.max_decimal_places}).' + ) + + def format_failure_message(self, total_violations, + violations_by_location, + format_page_link_fn=None): + if self.type == DocumentType.REPORT: + location_label = "Страница" + elif self.type == DocumentType.PRESENTATION: + location_label = "Слайд" + result_str = ( + f'Найдены числа с избыточным количеством десятичных знаков!
' + f'Максимально допустимое количество знаков после запятой: {self.max_decimal_places}
' + f'Всего нарушений: {total_violations}

' + ) + + for location_num, violations in violations_by_location.items(): + if format_page_link_fn: + location_str = f'{location_label} {format_page_link_fn([location_num])}' + else: + location_str = f'{location_label} №{location_num}' + + result_str += f'{location_str}:
' + result_str += '
'.join(violations) + result_str += '

' + + return result_str