From 50b3dfabd0f78b6e9cd8c053c52bd64dfe400114 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 10:47:14 +0200 Subject: [PATCH 01/15] feat: add fr.siren.format method --- stdnum/fr/siren.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stdnum/fr/siren.py b/stdnum/fr/siren.py index e3607475..0150b449 100644 --- a/stdnum/fr/siren.py +++ b/stdnum/fr/siren.py @@ -83,3 +83,9 @@ def to_tva(number: str) -> str: int(compact(number) + '12') % 97, ' ' if ' ' in number else '', number) + +def format(number: str, separator: str = ' ') -> str: + """Reformat the number to the standard presentation format.""" + number = compact(number) + return separator.join((number[0:3], number[3:6], number[6:9])) + From e017984bae830b6202d27a5e67b31aae3ad8de37 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 10:48:05 +0200 Subject: [PATCH 02/15] feat: add fr.rcs module to validate RCS number, format it and extract its siren number API consistent with other fr modules --- stdnum/fr/rcs.py | 109 ++++++++++++++++++++++++++++++++++++++ tests/test_fr_rcs.doctest | 52 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 stdnum/fr/rcs.py create mode 100644 tests/test_fr_rcs.doctest diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py new file mode 100644 index 00000000..387583a5 --- /dev/null +++ b/stdnum/fr/rcs.py @@ -0,0 +1,109 @@ +# rcs.py - functions for handling French NIR numbers +# coding: utf-8 +# +# Copyright (C) 2025 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""RCS (French registration number for commercial companies). + +The RCS number (Registre du commerce et des sociétés) is given by INSEE to a company with commercial activity +when created. It is required for most of administrative procedures. + +The number consists of "RCS" letters followed by name of the city where the company was registered +followed by letter A for a retailer or B for society +followed by the SIREN number + +More information: + +* https://entreprendre.service-public.fr/vosdroits/F31190 + +>>> validate('RCS Nancy B 323 159 715') +'RCS Nancy B 323 159 715' +>>> validate('RCS Nancy B 323159715') +'RCS Nancy B 323 159 715' +>>> validate('RCS Nancy B323 159715') +'RCS Nancy B 323 159 715' +>>> validate('RCS Nancy B 323 159 716') # invalid check digit +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> validate('6546546546546703') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('RCSNancy B 323 159 716') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('RCS NancyB 323 159 716') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> format('RCS Nancy B323159716') +'RCS Nancy B 323 159 715' +>>> to_siren("RCS Nancy B 323159 716") +"323159 716" +""" + +from __future__ import annotations +import re + +from stdnum.exceptions import * +from stdnum.fr import siren +from stdnum.util import clean + +RCS_VALIDATION_REGEX = r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" + +def validate(number:str) -> str: + match = re.match(RCS_VALIDATION_REGEX, number) + if not match: + raise InvalidFormat() + siren_number = siren.validate(match.group("siren")) + siren_number = siren.format(siren_number) + + return format(number) + + +def is_valid(number: str) -> bool: + """Check if the number provided is valid.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number: str) -> str: + """Reformat the number to the standard presentation format.""" + match = re.match(RCS_VALIDATION_REGEX, number) + if not match: + raise InvalidFormat() + return " ".join(("RCS", match.group('city'), match.group('letter'), siren.format(match.group('siren')))) + + +def to_siren(number:str)->str: + """Extract SIREN number from the RCS number. + + The SIREN number is the 9 last digits of the RCS number. + """ + _siren = [] + digit_count = 0 + for char in reversed(number): + if digit_count < 9: + _siren.insert(0,char) + if isdigits(char): + digit_count += 1 + return ''.join(_siren) diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest new file mode 100644 index 00000000..8530f208 --- /dev/null +++ b/tests/test_fr_rcs.doctest @@ -0,0 +1,52 @@ +test_fr_rcs.doctest - more detailed doctests for the stdnum.fr.rcs module + +Copyright (C) 2025 + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA + + +This file contains more detailed doctests for the stdnum.fr.rcs module. + +>>> from stdnum.fr import rcs +>>> from stdnum.exceptions import * + + +>>> rcs.validate('RCS Nancy B 323 159 715') +'RCS Nancy B 323 159 715' +>>> rcs.validate('RCS Nancy B 323159715') +'RCS Nancy B 323 159 715' +>>> rcs.validate('RCS Nancy B323 159715') +'RCS Nancy B 323 159 715' +>>> rcs.validate('RCS Nancy B 323 159 716') # invalid check digit +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> rcs.validate('6546546546546703') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> rcs.validate('RCSNancy B 323 159 716') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> rcs.validate('RCS NancyB 323 159 716') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> rcs.format('RCS Nancy B323159716') +'RCS Nancy B 323 159 715' +>>> to_siren("RCS Nancy B 323159 716") +"323159 716" From 8e74a14abc4e7ef11d3b6a70ad8eb67629e2a519 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 11:49:14 +0200 Subject: [PATCH 03/15] fix: missing isdigits import --- stdnum/fr/rcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 387583a5..2c9ad724 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -64,7 +64,7 @@ from stdnum.exceptions import * from stdnum.fr import siren -from stdnum.util import clean +from stdnum.util import isdigits RCS_VALIDATION_REGEX = r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" From c02789c53e66bbda08d92ef82d4a1865d567c489 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 11:49:19 +0200 Subject: [PATCH 04/15] style: type hint --- stdnum/fr/rcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 2c9ad724..26f88cc7 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -99,7 +99,7 @@ def to_siren(number:str)->str: The SIREN number is the 9 last digits of the RCS number. """ - _siren = [] + _siren: list[str] = [] digit_count = 0 for char in reversed(number): if digit_count < 9: From a93caf24d3d016c4f71daafaaef895a4cdbb6bfc Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 11:49:27 +0200 Subject: [PATCH 05/15] fix: remove useless test --- tests/test_fr_rcs.doctest | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest index 8530f208..4b6860b4 100644 --- a/tests/test_fr_rcs.doctest +++ b/tests/test_fr_rcs.doctest @@ -34,10 +34,6 @@ This file contains more detailed doctests for the stdnum.fr.rcs module. Traceback (most recent call last): ... InvalidChecksum: ... ->>> rcs.validate('6546546546546703') -Traceback (most recent call last): - ... -InvalidLength: ... >>> rcs.validate('RCSNancy B 323 159 716') # invalid format Traceback (most recent call last): ... From d8187661b8de15b543fa447a6e4e527fe211ee60 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 11:53:31 +0200 Subject: [PATCH 06/15] fix: ensure number is a string --- stdnum/fr/rcs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 26f88cc7..abd82036 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -64,12 +64,12 @@ from stdnum.exceptions import * from stdnum.fr import siren -from stdnum.util import isdigits +from stdnum.util import isdigits, clean RCS_VALIDATION_REGEX = r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" def validate(number:str) -> str: - match = re.match(RCS_VALIDATION_REGEX, number) + match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() siren_number = siren.validate(match.group("siren")) @@ -88,7 +88,7 @@ def is_valid(number: str) -> bool: def format(number: str) -> str: """Reformat the number to the standard presentation format.""" - match = re.match(RCS_VALIDATION_REGEX, number) + match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() return " ".join(("RCS", match.group('city'), match.group('letter'), siren.format(match.group('siren')))) From 35afeedc1310c4e3c1d0520ecc7150f29cb91106 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 11:57:05 +0200 Subject: [PATCH 07/15] fix: test --- tests/test_fr_rcs.doctest | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest index 4b6860b4..a49b86fb 100644 --- a/tests/test_fr_rcs.doctest +++ b/tests/test_fr_rcs.doctest @@ -42,7 +42,7 @@ InvalidFormat: ... Traceback (most recent call last): ... InvalidFormat: ... ->>> rcs.format('RCS Nancy B323159716') +>>> rcs.format('RCS Nancy B323159715') 'RCS Nancy B 323 159 715' ->>> to_siren("RCS Nancy B 323159 716") -"323159 716" +>>> to_siren("RCS Nancy B 323159 715") +"323159 715" From eb24165dc8d921dc71fccd06186e8d769ec1e451 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:00:59 +0200 Subject: [PATCH 08/15] style: code format --- stdnum/fr/rcs.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index abd82036..54a98384 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -20,7 +20,7 @@ """RCS (French registration number for commercial companies). -The RCS number (Registre du commerce et des sociétés) is given by INSEE to a company with commercial activity +The RCS number (Registre du commerce et des sociétés) is given by INSEE to a company with commercial activity when created. It is required for most of administrative procedures. The number consists of "RCS" letters followed by name of the city where the company was registered @@ -60,15 +60,23 @@ """ from __future__ import annotations + import re from stdnum.exceptions import * from stdnum.fr import siren -from stdnum.util import isdigits, clean +from stdnum.util import clean, isdigits + + +RCS_VALIDATION_REGEX = ( + r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" +) -RCS_VALIDATION_REGEX = r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" -def validate(number:str) -> str: +def validate(number: str) -> str: + """ + Validate number is a valid french RCS number. + """ match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() @@ -91,10 +99,17 @@ def format(number: str) -> str: match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() - return " ".join(("RCS", match.group('city'), match.group('letter'), siren.format(match.group('siren')))) + return " ".join( + ( + "RCS", + match.group("city"), + match.group("letter"), + siren.format(match.group("siren")), + ) + ) -def to_siren(number:str)->str: +def to_siren(number: str) -> str: """Extract SIREN number from the RCS number. The SIREN number is the 9 last digits of the RCS number. @@ -103,7 +118,7 @@ def to_siren(number:str)->str: digit_count = 0 for char in reversed(number): if digit_count < 9: - _siren.insert(0,char) + _siren.insert(0, char) if isdigits(char): digit_count += 1 - return ''.join(_siren) + return "".join(_siren) From 5de14f64ecf57e7820edd60975c1f81e44968dc5 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:06:51 +0200 Subject: [PATCH 09/15] style: code format --- stdnum/fr/rcs.py | 22 ++++++++++------------ stdnum/fr/siren.py | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 54a98384..4ee34460 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -69,18 +69,16 @@ RCS_VALIDATION_REGEX = ( - r"^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$" + r'^ *(?PRCS|rcs) +(?P.*?) +(?P[AB]) *(?P(?:\d *){9})\b *$' ) def validate(number: str) -> str: - """ - Validate number is a valid french RCS number. - """ + """Validate number is a valid french RCS number.""" match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() - siren_number = siren.validate(match.group("siren")) + siren_number = siren.validate(match.group('siren')) siren_number = siren.format(siren_number) return format(number) @@ -99,13 +97,13 @@ def format(number: str) -> str: match = re.match(RCS_VALIDATION_REGEX, clean(number)) if not match: raise InvalidFormat() - return " ".join( + return ' '.join( ( - "RCS", - match.group("city"), - match.group("letter"), - siren.format(match.group("siren")), - ) + 'RCS', + match.group('city'), + match.group('letter'), + siren.format(match.group('siren')), + ), ) @@ -121,4 +119,4 @@ def to_siren(number: str) -> str: _siren.insert(0, char) if isdigits(char): digit_count += 1 - return "".join(_siren) + return ''.join(_siren) diff --git a/stdnum/fr/siren.py b/stdnum/fr/siren.py index 0150b449..63d7732b 100644 --- a/stdnum/fr/siren.py +++ b/stdnum/fr/siren.py @@ -84,8 +84,8 @@ def to_tva(number: str) -> str: ' ' if ' ' in number else '', number) + def format(number: str, separator: str = ' ') -> str: """Reformat the number to the standard presentation format.""" number = compact(number) return separator.join((number[0:3], number[3:6], number[6:9])) - From 02fc4a45e1007b67e85a4c6e70d8b1e1e1a1e2ac Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:11:01 +0200 Subject: [PATCH 10/15] fix: test --- tests/test_fr_rcs.doctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest index a49b86fb..bcc93003 100644 --- a/tests/test_fr_rcs.doctest +++ b/tests/test_fr_rcs.doctest @@ -44,5 +44,5 @@ Traceback (most recent call last): InvalidFormat: ... >>> rcs.format('RCS Nancy B323159715') 'RCS Nancy B 323 159 715' ->>> to_siren("RCS Nancy B 323159 715") +>>> rcs.to_siren("RCS Nancy B 323159 715") "323159 715" From a2c03567ed34ac018cbe944f6aa7c550d3e9f132 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:12:05 +0200 Subject: [PATCH 11/15] fix: test --- tests/test_fr_rcs.doctest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest index bcc93003..790d697a 100644 --- a/tests/test_fr_rcs.doctest +++ b/tests/test_fr_rcs.doctest @@ -45,4 +45,4 @@ InvalidFormat: ... >>> rcs.format('RCS Nancy B323159715') 'RCS Nancy B 323 159 715' >>> rcs.to_siren("RCS Nancy B 323159 715") -"323159 715" +'323159 715' From 918257ed469b120767a09847069359d907a47dbc Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:17:05 +0200 Subject: [PATCH 12/15] fix: test --- stdnum/fr/rcs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 4ee34460..3f8dbd0a 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -41,10 +41,6 @@ Traceback (most recent call last): ... InvalidChecksum: ... ->>> validate('6546546546546703') -Traceback (most recent call last): - ... -InvalidLength: ... >>> validate('RCSNancy B 323 159 716') # invalid format Traceback (most recent call last): ... From f03f45d6c3eece1872b51bc6897d98aad4696f77 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:20:54 +0200 Subject: [PATCH 13/15] fix: test --- stdnum/fr/rcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index 3f8dbd0a..abf2d97b 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -49,9 +49,9 @@ Traceback (most recent call last): ... InvalidFormat: ... ->>> format('RCS Nancy B323159716') +>>> format('RCS Nancy B323159715') 'RCS Nancy B 323 159 715' ->>> to_siren("RCS Nancy B 323159 716") +>>> to_siren("RCS Nancy B 323159 715") "323159 716" """ From 6dbfaf1cebb6b509c7de2cb7dc62d4ffad8fc5cc Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:26:38 +0200 Subject: [PATCH 14/15] fix: test --- stdnum/fr/rcs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stdnum/fr/rcs.py b/stdnum/fr/rcs.py index abf2d97b..4debb98f 100644 --- a/stdnum/fr/rcs.py +++ b/stdnum/fr/rcs.py @@ -51,8 +51,12 @@ InvalidFormat: ... >>> format('RCS Nancy B323159715') 'RCS Nancy B 323 159 715' +>>> format('bad') +Traceback (most recent call last): + ... +InvalidFormat: ... >>> to_siren("RCS Nancy B 323159 715") -"323159 716" +'323159 715' """ from __future__ import annotations From fdc9f5f94bbb0c0f416ee7c2d53f12af0c867ae8 Mon Sep 17 00:00:00 2001 From: Fabien MICHEL Date: Fri, 8 Aug 2025 12:28:31 +0200 Subject: [PATCH 15/15] fix: remove useless test file --- tests/test_fr_rcs.doctest | 48 --------------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 tests/test_fr_rcs.doctest diff --git a/tests/test_fr_rcs.doctest b/tests/test_fr_rcs.doctest deleted file mode 100644 index 790d697a..00000000 --- a/tests/test_fr_rcs.doctest +++ /dev/null @@ -1,48 +0,0 @@ -test_fr_rcs.doctest - more detailed doctests for the stdnum.fr.rcs module - -Copyright (C) 2025 - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA - - -This file contains more detailed doctests for the stdnum.fr.rcs module. - ->>> from stdnum.fr import rcs ->>> from stdnum.exceptions import * - - ->>> rcs.validate('RCS Nancy B 323 159 715') -'RCS Nancy B 323 159 715' ->>> rcs.validate('RCS Nancy B 323159715') -'RCS Nancy B 323 159 715' ->>> rcs.validate('RCS Nancy B323 159715') -'RCS Nancy B 323 159 715' ->>> rcs.validate('RCS Nancy B 323 159 716') # invalid check digit -Traceback (most recent call last): - ... -InvalidChecksum: ... ->>> rcs.validate('RCSNancy B 323 159 716') # invalid format -Traceback (most recent call last): - ... -InvalidFormat: ... ->>> rcs.validate('RCS NancyB 323 159 716') # invalid format -Traceback (most recent call last): - ... -InvalidFormat: ... ->>> rcs.format('RCS Nancy B323159715') -'RCS Nancy B 323 159 715' ->>> rcs.to_siren("RCS Nancy B 323159 715") -'323159 715'