Skip to content

Commit 913fd99

Browse files
committed
initial commit
0 parents  commit 913fd99

File tree

5 files changed

+551
-0
lines changed

5 files changed

+551
-0
lines changed

.gitignore

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Linux template
3+
*~
4+
5+
# KDE directory preferences
6+
.directory
7+
8+
# Linux trash folder which might appear on any partition or disk
9+
.Trash-*
10+
11+
12+
### JetBrains template
13+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
14+
15+
*.iml
16+
17+
## Directory-based project format:
18+
.idea/
19+
# if you remove the above rule, at least ignore the following:
20+
21+
# User-specific stuff:
22+
# .idea/workspace.xml
23+
# .idea/tasks.xml
24+
# .idea/dictionaries
25+
26+
# Sensitive or high-churn files:
27+
# .idea/dataSources.ids
28+
# .idea/dataSources.xml
29+
# .idea/sqlDataSources.xml
30+
# .idea/dynamic.xml
31+
# .idea/uiDesigner.xml
32+
33+
# Gradle:
34+
# .idea/gradle.xml
35+
# .idea/libraries
36+
37+
# Mongo Explorer plugin:
38+
# .idea/mongoSettings.xml
39+
40+
## File-based project format:
41+
*.ipr
42+
*.iws
43+
44+
## Plugin-specific files:
45+
46+
# IntelliJ
47+
/out/
48+
49+
# mpeltonen/sbt-idea plugin
50+
.idea_modules/
51+
52+
# JIRA plugin
53+
atlassian-ide-plugin.xml
54+
55+
# Crashlytics plugin (for Android Studio and IntelliJ)
56+
com_crashlytics_export_strings.xml
57+
crashlytics.properties
58+
crashlytics-build.properties
59+
60+
61+
### VirtualEnv template
62+
# Virtualenv
63+
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
64+
.Python
65+
[Bb]in
66+
[Ii]nclude
67+
[Ll]ib
68+
[Ss]cripts
69+
pyvenv.cfg
70+
pip-selfcheck.json
71+
72+
73+
### Python cache
74+
75+
*.pyc
76+
__pycache__

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Python String Utils
2+
3+
A small utility library for strings.
4+
5+
- simple and pythonic
6+
- PEP8 complaint
7+
- 100% code coverage
8+
- works with python 2.7+ and 3+

setup.py

Whitespace-only changes.

string_utils.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import re
2+
3+
# module settings
4+
__version__ = '0.0.0'
5+
__all__ = [
6+
'is_email',
7+
'is_credit_card',
8+
'is_camel_case',
9+
'is_snake_case',
10+
'camel_case_to_snake',
11+
'snake_case_to_camel',
12+
'reverse',
13+
]
14+
15+
# compiled regex
16+
EMAIL_RE = re.compile('^[a-zA-Z\d\._\+-]+@([a-z\d-]+\.?[a-z\d-]+)+\.[a-z]{2,4}$')
17+
CAMEL_CASE_TEST_RE = re.compile('^[a-zA-Z]*([a-z]+[A-Z]+|[A-Z]+[a-z]+)[a-zA-Z\d]*$')
18+
CAMEL_CASE_REPLACE_RE = re.compile('([a-z]|[A-Z]+)(?=[A-Z])')
19+
SNAKE_CASE_TEST_RE = re.compile('^[a-z]+([a-z\d]+_|_[a-z\d]+)+[a-z\d]+$')
20+
SNAKE_CASE_TEST_DASH_RE = re.compile('^[a-z]+([a-z\d]+-|-[a-z\d]+)+[a-z\d]+$')
21+
SNAKE_CASE_REPLACE_RE = re.compile('(_)([a-z\d])')
22+
SNAKE_CASE_REPLACE_DASH_RE = re.compile('(-)([a-z\d])')
23+
CREDIT_CARDS = {
24+
'VISA': re.compile('^4[0-9]{12}(?:[0-9]{3})?$'),
25+
'MASTERCARD': re.compile('^5[1-5][0-9]{14}$'),
26+
'AMERICAN_EXPRESS': re.compile('^3[47][0-9]{13}$'),
27+
'DINERS_CLUB': re.compile('^3(?:0[0-5]|[68][0-9])[0-9]{11}$'),
28+
'DISCOVER': re.compile('^6(?:011|5[0-9]{2})[0-9]{12}$'),
29+
'JCB': re.compile('^(?:2131|1800|35\d{3})\d{11}$')
30+
}
31+
32+
33+
# string checking functions
34+
35+
def is_email(string):
36+
"""
37+
Returns true if the string is a valid email.
38+
IMPORTANT NOTES:
39+
By design, the implementation of this checking does not follow the specification for a valid
40+
email address, but instead it's based on real world cases in order to match more than 99%
41+
of emails and catch user mistakes. For example the percentage sign "%" is a valid sign for an email,
42+
but actually no one use it, instead if such sign is found in a string coming from user input (like a
43+
web form) is very likely that the intention was to type "5" (which is on the same key on a US keyboard).
44+
You can take a look at "IsEmailTestCase" in tests.py for further details.
45+
46+
:param string: String to check
47+
:return: True if email, false otherwise
48+
"""
49+
return bool(EMAIL_RE.match(string))
50+
51+
52+
def is_credit_card(string, card_type=None):
53+
if card_type:
54+
if card_type not in CREDIT_CARDS:
55+
raise KeyError(
56+
'Invalid card type "%s". Valid types are: %s' % (card_type, ', '.join(CREDIT_CARDS.keys()))
57+
)
58+
return bool(CREDIT_CARDS[card_type].match(string))
59+
for c in CREDIT_CARDS:
60+
if CREDIT_CARDS[c].match(string):
61+
return True
62+
return False
63+
64+
65+
def is_camel_case(string):
66+
"""
67+
Checks if a string is formatted as camel case.
68+
A string is considered camel case when:
69+
- its composed only by letters ([a-zA-Z]) and optionally numbers ([0-9])
70+
- it contains both lowercase and uppercase letters
71+
- it does not start with a number
72+
73+
:param string: String to test.
74+
:return: True for a camel case string, false otherwise.
75+
"""
76+
return bool(CAMEL_CASE_TEST_RE.match(string))
77+
78+
79+
def is_snake_case(string, separator='_'):
80+
"""
81+
Checks if a string is formatted as snake case.
82+
A string is considered snake case when:
83+
- its composed only by lowercase letters ([a-z]), underscores (or provided separator) and
84+
optionally numbers ([0-9])
85+
- it does not start/end with an underscore (or provided separator)
86+
- it does not start with a number
87+
88+
:param string: String to test.
89+
:return: True for a snake case string, false otherwise.
90+
"""
91+
re_map = {
92+
'_': SNAKE_CASE_TEST_RE,
93+
'-': SNAKE_CASE_TEST_DASH_RE
94+
}
95+
re_template = '^[a-z]+([a-z\d]+{sign}|{sign}[a-z\d]+)+[a-z\d]+$'
96+
r = re_map.get(separator, re.compile(re_template.format(sign=re.escape(separator))))
97+
return bool(r.match(string))
98+
99+
100+
# string manipulation functions
101+
102+
def reverse(string):
103+
"""
104+
Returns the string reversed ("abc" -> "cba").
105+
106+
:param string: String to revert.
107+
:return: Reversed string.
108+
"""
109+
return ''.join(list(reversed(string)))
110+
111+
112+
# def shuffle(string):
113+
# pass
114+
#
115+
#
116+
# def is_multiline(string):
117+
# pass
118+
#
119+
#
120+
# def is_url(string):
121+
# pass
122+
#
123+
#
124+
# def is_zip_code(string, country_code=None):
125+
# pass
126+
127+
128+
def camel_case_to_snake(string, separator='_'):
129+
"""
130+
Convert a camel case string into a snake case one.
131+
(The original string is returned if is not a valid camel case string)
132+
133+
:param string: String to convert.
134+
:param separator: Sign to use as separator.
135+
:return: Converted string
136+
"""
137+
if not is_camel_case(string):
138+
return string
139+
return CAMEL_CASE_REPLACE_RE.sub(lambda m: m.group(1) + separator, string).lower()
140+
141+
142+
def snake_case_to_camel(string, upper_case_first=True, separator='_'):
143+
"""
144+
Convert a snake case string into a camel case one.
145+
(The original string is returned if is not a valid snake case string)
146+
147+
:param string: String to convert.
148+
:param upper_case_first: True to turn the first letter into uppercase (default).
149+
:param separator: Sign to use as separator (default to "_").
150+
:return: Converted string
151+
"""
152+
if not is_snake_case(string, separator):
153+
return string
154+
re_map = {
155+
'_': SNAKE_CASE_REPLACE_RE,
156+
'-': SNAKE_CASE_REPLACE_DASH_RE
157+
}
158+
r = re_map.get(separator, re.compile('({sign})([a-z\d])'.format(sign=re.escape(separator))))
159+
string = r.sub(lambda m: m.group(2).upper(), string)
160+
if upper_case_first:
161+
return string[0].upper() + string[1:]
162+
return string

0 commit comments

Comments
 (0)