Skip to content
This repository was archived by the owner on Dec 26, 2025. It is now read-only.

Commit 0d1a9a8

Browse files
committed
Added regex format checks to util
1 parent 66ae873 commit 0d1a9a8

File tree

2 files changed

+188
-2
lines changed

2 files changed

+188
-2
lines changed

src/adif_file/util.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
33

44
"""Provides some useful function to handle ADIF data"""
5-
5+
import re
66
import sys
77
import datetime
88

@@ -17,16 +17,89 @@ def get_cur_adif_dt() -> str:
1717

1818
def adif_date2iso(date: str) -> str:
1919
"""Tries to convert ADIF date to iso format"""
20+
# Todo: should raise a ValueError if wrong format
2021
if not date or len(date) != 8:
2122
return date
2223
return date[:4] + '-' + date[4:6] + '-' + date[6:8]
2324

2425

2526
def adif_time2iso(time: str) -> str:
2627
"""Tries to convert ADIF time to iso format"""
28+
# Todo: should raise a ValueError if wrong format
2729
if not time or len(time) not in (4, 6):
2830
return time
2931
return time[:2] + ':' + time[2:4] + (':' + time[4:6] if len(time) == 6 else '')
3032

3133

32-
__all__ = ['get_cur_adif_dt', 'adif_date2iso', 'adif_time2iso']
34+
def iso_date2adif(date: str) -> str:
35+
"""Converts an ISO formated date to ADIF"""
36+
if not check_format(REGEX_ISODATE, date):
37+
raise ValueError(f'Not a valide ISO date "{date}"')
38+
return date.replace('-', '')
39+
40+
41+
def iso_time2adif(time: str) -> str:
42+
"""Converts an ISO formated time to ADIF"""
43+
if not check_format(REGEX_ISOTIME, time):
44+
raise ValueError(f'Not a valide ISO time "{time}"')
45+
return time.replace(':', '')
46+
47+
48+
REGEX_CALL = re.compile(r'([a-zA-Z0-9]{1,3}?/)?([a-zA-Z0-9]{1,3}?[0-9][a-zA-Z0-9]{0,3}?[a-zA-Z])(/[aAmMpPrRtT]{1,2}?)?')
49+
REGEX_RST = re.compile(r'([1-5]([1-9]([1-9][aAcCkKmMsSxX]?)?)?)|([-+][0-9]{1,2})')
50+
REGEX_LOCATOR = re.compile(r'[a-rA-R]{2}[0-9]{2}([a-xA-X]{2}([0-9]{2})?)?')
51+
REGEX_NONASCII = re.compile(r'[ -~\n\r]*(.)?')
52+
REGEX_ADIFTIME = re.compile(r'(([0-1][0-9])|(2[0-3]))([0-5][0-9])([0-5][0-9])?')
53+
REGEX_ADIFDATE = re.compile(r'([1-9][0-9]{3})((0[1-9])|(1[0-2]))((0[1-9])|([1-2][0-9])|(3[0-1]))')
54+
REGEX_ISOTIME = re.compile(r'(([0-1][0-9])|(2[0-3])):([0-5][0-9])(:[0-5][0-9])?')
55+
REGEX_ISODATE = re.compile(r'([1-9][0-9]{3})-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2][0-9])|(3[0-1]))')
56+
# noinspection RegExpRedundantEscape
57+
REGEX_EMAIL = re.compile(r'[\w\-\.]+@([\w-]+\.)+[\w-]{2,}')
58+
59+
60+
def check_format(exp: re.Pattern, txt: str) -> bool:
61+
"""Test the given text against a regular expression
62+
:param exp: a compiled pattern (e.g. util.REGEX_*)
63+
:param txt: a text
64+
:return: true if pattern matches"""
65+
return bool(re.fullmatch(exp, txt))
66+
67+
68+
def check_call(call: str) -> None | tuple:
69+
"""Test a call sign against a regular expression
70+
:param call: a call sign
71+
:return: tuple of parts ('Country prefix/', 'Call sign', '/Operation suffix')"""
72+
m = re.fullmatch(REGEX_CALL, call)
73+
if m:
74+
return m.groups()
75+
return None
76+
77+
78+
def find_non_ascii(text: str) -> set:
79+
"""Find all non ASCII chars in a text
80+
:param text: the text to search in
81+
:return: a set of non ASCII chars"""
82+
non_ascii = []
83+
for c in re.findall(REGEX_NONASCII, text):
84+
if c:
85+
non_ascii.append(c)
86+
return set(non_ascii)
87+
88+
89+
def replace_non_ascii(text: str, replace: dict[str, str] = None, default: str = '_') -> str:
90+
"""Replaces every non ASCII char with a str from a mapping
91+
:param text: the text containing non ASCII chars
92+
:param replace: a mapping with non ASCII chars and suiting substitutes (e.g. {'ä':'ae', 'ß':'ss'})
93+
:param default: the default substitute if no mapping is found
94+
:return: text with substitutes"""
95+
replace = replace if type(replace) is dict else {}
96+
default = default if type(default) is str else '_'
97+
for na in find_non_ascii(text):
98+
text = text.replace(na, replace.get(na, default))
99+
return text
100+
101+
102+
__all__ = ['get_cur_adif_dt', 'adif_date2iso', 'adif_time2iso', 'iso_date2adif', 'iso_time2adif',
103+
'check_format', 'check_call', 'replace_non_ascii',
104+
'REGEX_ADIFDATE', 'REGEX_ADIFTIME', 'REGEX_ISODATE', 'REGEX_ISOTIME',
105+
'REGEX_EMAIL', 'REGEX_RST', 'REGEX_LOCATOR', 'REGEX_CALL']

test/test_util.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# PyADIF-File (c) 2025 by Andreas Schawo is licensed under CC BY-SA 4.0.
2+
# To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
3+
4+
import unittest
5+
6+
from adif_file.util import *
7+
8+
9+
class Util(unittest.TestCase):
10+
def test_10_conv_adfidate(self):
11+
self.assertEqual('2025-04-25', adif_date2iso('20250425'))
12+
self.assertEqual('2025042', adif_date2iso('2025042')) # Todo: should raise an ValueError
13+
14+
def test_20_conv_adfitime(self):
15+
self.assertEqual('11:25', adif_time2iso('1125'))
16+
self.assertEqual('18:33:45', adif_time2iso('183345'))
17+
self.assertEqual('18334', adif_time2iso('18334')) # Todo: should raise an ValueError
18+
19+
def test_30_conv_isodate(self):
20+
self.assertEqual('20250425', iso_date2adif('2025-04-25'))
21+
self.assertRaises(ValueError, iso_date2adif, '2025-042')
22+
23+
def test_40_conv_isotime(self):
24+
self.assertEqual('1125', iso_time2adif('11:25'))
25+
self.assertEqual('183345', iso_time2adif('18:33:45'))
26+
self.assertRaises(ValueError, iso_time2adif, '20:2542')
27+
self.assertRaises(ValueError, iso_time2adif, '7:25')
28+
29+
def test_100_call_format(self):
30+
self.assertTrue(check_format(REGEX_CALL, 'Df1ASC'))
31+
self.assertTrue(check_format(REGEX_CALL, 'DF1ASc/P'))
32+
self.assertTrue(check_format(REGEX_CALL, 'HB9/dF1ASC'))
33+
self.assertFalse(check_format(REGEX_CALL, 'HB9/DF1ASC/MOBILE'))
34+
self.assertFalse(check_format(REGEX_CALL, 'DF1 ASC'))
35+
self.assertTrue(check_format(REGEX_CALL, '2N2XCV'))
36+
self.assertTrue(check_format(REGEX_CALL, 'N2X'))
37+
38+
def test_105_check_call(self):
39+
self.assertTupleEqual((None, 'DF1ASC', None), check_call('DF1ASC'))
40+
self.assertTupleEqual((None, 'DF1ASC', '/m'), check_call('DF1ASC/m'))
41+
self.assertTupleEqual(('HB9/', 'DF1ASC', None), check_call('HB9/DF1ASC'))
42+
self.assertTupleEqual(('HB9/', 'DF1ASC', '/MM'), check_call('HB9/DF1ASC/MM'))
43+
self.assertIsNone(check_call('HB9/DF1ASC/MOBILE'))
44+
self.assertIsNone(check_call('DF1 ASC'))
45+
46+
def test_110_adifdate_format(self):
47+
self.assertTrue(check_format(REGEX_ADIFDATE, '20251231'))
48+
self.assertFalse(check_format(REGEX_ADIFDATE, '2025-12-31'))
49+
self.assertFalse(check_format(REGEX_ADIFDATE, '251231'))
50+
self.assertFalse(check_format(REGEX_ADIFDATE, '20251331'))
51+
self.assertFalse(check_format(REGEX_ADIFDATE, '20250132'))
52+
53+
def test_120_adiftime_format(self):
54+
self.assertTrue(check_format(REGEX_ADIFTIME, '2359'))
55+
self.assertTrue(check_format(REGEX_ADIFTIME, '235959'))
56+
self.assertFalse(check_format(REGEX_ADIFTIME, '617'))
57+
self.assertFalse(check_format(REGEX_ADIFTIME, '06:17'))
58+
self.assertFalse(check_format(REGEX_ADIFTIME, '6:17'))
59+
self.assertFalse(check_format(REGEX_ADIFTIME, '1260'))
60+
61+
def test_130_isodate_format(self):
62+
self.assertTrue(check_format(REGEX_ISODATE, '2025-12-31'))
63+
self.assertFalse(check_format(REGEX_ISODATE, '20251231'))
64+
self.assertFalse(check_format(REGEX_ISODATE, '25-12-31'))
65+
self.assertFalse(check_format(REGEX_ISODATE, '2025-13-31'))
66+
self.assertFalse(check_format(REGEX_ISODATE, '2025-01-32'))
67+
68+
def test_140_isotime_format(self):
69+
self.assertTrue(check_format(REGEX_ISOTIME, '23:59'))
70+
self.assertTrue(check_format(REGEX_ISOTIME, '23:59:59'))
71+
self.assertFalse(check_format(REGEX_ISOTIME, '6:17'))
72+
self.assertFalse(check_format(REGEX_ISOTIME, '0617'))
73+
self.assertFalse(check_format(REGEX_ISOTIME, '617'))
74+
self.assertFalse(check_format(REGEX_ISOTIME, '12:60'))
75+
76+
def test_150_locator_format(self):
77+
self.assertTrue(check_format(REGEX_LOCATOR, 'aa11'))
78+
self.assertTrue(check_format(REGEX_LOCATOR, 'aa11gg'))
79+
self.assertTrue(check_format(REGEX_LOCATOR, 'aa11gg99'))
80+
self.assertTrue(check_format(REGEX_LOCATOR, 'aa00ff'))
81+
self.assertTrue(check_format(REGEX_LOCATOR, 'RR99xx'))
82+
self.assertFalse(check_format(REGEX_LOCATOR, 'aa99zz'))
83+
self.assertFalse(check_format(REGEX_LOCATOR, 'zz99aa'))
84+
self.assertFalse(check_format(REGEX_LOCATOR, 'aa54e'))
85+
self.assertFalse(check_format(REGEX_LOCATOR, 'aa5ee'))
86+
87+
def test_160_rst_format(self):
88+
self.assertTrue(check_format(REGEX_RST, '59'))
89+
self.assertTrue(check_format(REGEX_RST, '599'))
90+
self.assertTrue(check_format(REGEX_RST, '597m'))
91+
self.assertTrue(check_format(REGEX_RST, '111A'))
92+
self.assertFalse(check_format(REGEX_RST, '11A'))
93+
self.assertFalse(check_format(REGEX_RST, '01'))
94+
self.assertFalse(check_format(REGEX_RST, '10'))
95+
96+
# Digi modes
97+
self.assertTrue(check_format(REGEX_RST, '12'))
98+
self.assertTrue(check_format(REGEX_RST, '+12'))
99+
self.assertTrue(check_format(REGEX_RST, '+6'))
100+
self.assertTrue(check_format(REGEX_RST, '-5'))
101+
self.assertTrue(check_format(REGEX_RST, '-35'))
102+
self.assertFalse(check_format(REGEX_RST, '+100'))
103+
self.assertFalse(check_format(REGEX_RST, '-223'))
104+
105+
def test_200_replace_nonascii(self):
106+
txt_non_ascii = 'Es saß ´ne hübsche YL am Funkegrät und funkte, nach Östereich auf 70cm sie blödelte und unkte'
107+
txt_ascii_default = 'Es sa_ _ne h_bsche YL am Funkegr_t und funkte, nach _stereich auf 70cm sie bl_delte und unkte'
108+
txt_ascii_defsharp = 'Es sa# #ne h#bsche YL am Funkegr#t und funkte, nach #stereich auf 70cm sie bl#delte und unkte'
109+
txt_ascii_de = 'Es sass \'ne huebsche YL am Funkegraet und funkte, nach Oestereich auf 70cm sie bloedelte und unkte'
110+
ascii_map = {'ß':'ss', '´':'\'', 'ä': 'ae', 'ö': 'oe', 'Ö': 'Oe', 'ü': 'ue'}
111+
self.assertEqual(txt_ascii_default, replace_non_ascii(txt_non_ascii))
112+
self.assertEqual(txt_ascii_defsharp, replace_non_ascii(txt_non_ascii, default='#'))
113+
self.assertEqual(txt_ascii_de, replace_non_ascii(txt_non_ascii, ascii_map))

0 commit comments

Comments
 (0)