How to exclude a sign of equality in regular expressions? - python

I write a parser in Python. For this I use regular expressions. But here it is necessary to eliminate an equal sign.
_PIN_PATTERN = '(^[a-zA-Z]+[0-9]*[:]?[a-zA-Z0-9]*)'
_ATTRIBUTE_PATTERN = '(^[a-zA-Z0-9_]+[0-9]*(\s)*[=](\s)*[a-z0-9.-]+)'
The fact that _PIN_PATTERN and _ATTRIBUTE_PATTERN in this case, return the same value, for example ('ATTRIBUTE', 'ad = 0.013p') ('PIN', 'ad = 0.013p') . But _PIN_PATTERN should not return ('PIN', 'ad = 0.013p'), it should return something like ('PIN', 'VSS:F85'), that is without an equal sign.
That's part of my code
def _is_pin(self, s):
a = re.match(self._PIN_PATTERN, s)
e = re.match(r'=', s)
if a and not e:
return True
else:
return False

Are you looking for something like this:
_ATTRIBUTE_PATTERN = r'[\w_]+\s*=\s*[\w\.\-]+' # matches ad = 0.013p
# _ATTRIBUTE_PATTERN = r'[^=]+=[^=]+' # matches string containing '='
_PIN_PATTERN = r'\w+\s*:\s*\w+' # matches VSS:F85
# _PIN_PATTERN = r'[^=]+' # if you want to match any string which does not contain '=' char

Related

Iterating with multiple variables

I am taking a course from Georgia Tech and I have spent all my evening trying to figure this out and I havent been able to do so. My task is as follows:
Write a function called my_TAs. The function should take as
input three strings: first_TA, second_TA, and third_TA. It
should return as output the string, "[first_TA], [second_TA],#and [third_TA] are awesome!", with the values replacing the
variable names.
For example, my_TAs("Sridevi", "Lucy", "Xu") would return
the string "Sridevi, Lucy, and Xu are awesome!".
Hint: Notice that because you're returning a string instead
of printing a string, you can't use the print() statement
-- you'll have to create the string yourself, then return it.
My function returns "Joshua are awesome" instead of all three variables names. I tried this
result = str(first_TA), str(second_TA), str(third_TA) + "are awesome!"
but didn't work.
def my_TAs(first_TA, second_TA, third_TA):
result = str(first_TA) + " are Awesome!"
return result
first_TA = "Joshua"
second_TA = "Jackie"
third_TA = "Marguerite"
test_first_TA = "Joshua"
test_second_TA = "Jackie"
test_third_TA = "Marguerite"
print(my_TAs(test_first_TA, test_second_TA, test_third_TA))
You can use f-Strings to accomplish this:
def my_TAs(first_TA, second_TA, third_TA):
return f"{first_TA}, {second_TA}, and {third_TA} are awesome!"
test_first_TA = "Joshua"
test_second_TA = "Jackie"
test_third_TA = "Marguerite"
print(my_TAs(test_first_TA, test_second_TA, test_third_TA))
Output:
Joshua, Jackie, and Marguerite are awesome!
Use + instead of ,
def my_TAs(first_TA, second_TA, third_TA):
result = str(first_TA) + ", " + str(second_TA) + ", and " + str(third_TA)
+ " are Awesome!"
return result
first_TA = "Joshua"
second_TA = "Jackie"
third_TA = "Marguerite"
test_first_TA = "Joshua"
test_second_TA = "Jackie"
test_third_TA = "Marguerite"
print(my_TAs(test_first_TA, test_second_TA, test_third_TA))

Check string for specific format of substring, how to..?

Two strings. My items name:
Parfume name EDT 50ml
And competitor's items name:
Parfume another name EDP 60ml
And i have a long list of these names in one column, competitors names in other column, and I want to leave only those rows in dataframe, that have same amount of ml in both my and competitors names no matter what everything else in these strings look like. So how do I find a substring ending with 'ml' in a bigger string? I could simply do
"**ml" in competitors_name
to see if they both contain the same amount of ml.
Thank you
UPDATE
'ml' is not always at the end of string. It might look like this
Parfume yet another great name 60ml EDP
Try this:
import re
def same_measurement(my_item, competitor_item, unit="ml"):
matcher = re.compile(r".*?(\d+){}".format(unit))
my_match = matcher.match(my_item)
competitor_match = matcher.match(competitor_item)
return my_match and competitor_match and my_match.group(1) == competitor_match.group(1)
my_item = "Parfume name EDT 50ml"
competitor_item = "Parfume another name EDP 50ml"
assert same_measurement(my_item, competitor_item)
my_item = "Parfume name EDT 50ml"
competitor_item = "Parfume another name EDP 60ml"
assert not same_measurement(my_item, competitor_item)
You could use the python Regex library to select the 'xxml' values for each of your data rows and then do some logic to check if they match.
import re
data_rows = [["Parfume name EDT", "Parfume another name EDP 50ml"]]
for data_pairs in data_rows:
my_ml = None
comp_ml = None
# Check for my ml matches and set value
my_ml_matches = re.search(r'(\d{1,3}[Mm][Ll])', data_pairs[0])
if my_ml_matches != None:
my_ml = my_ml_matches[0]
else:
print("my_ml has no ml")
# Check for comp ml matches and set value
comp_ml_matches = re.search(r'(\d{1,3}[Mm][Ll])', data_pairs[1])
if comp_ml_matches != None:
comp_ml = comp_ml_matches[0]
else:
print("comp_ml has no ml")
# Print outputs
if (my_ml != None) and (comp_ml != None):
if my_ml == comp_ml:
print("my_ml: {0} == comp_ml: {1}".format(my_ml, comp_ml))
else:
print("my_ml: {0} != comp_ml: {1}".format(my_ml, comp_ml))
Where data_rows = each row in the data set
Where data_pairs = {your_item_name, competitor_item_name}
You could use a lambda function to do that.
import pandas as pd
import re
d = {
'Us':
['Parfume one 50ml', 'Parfume two 100ml'],
'Competitor':
['Parfume uno 50ml', 'Parfume dos 200ml']
}
df = pd.DataFrame(data=d)
df['Eq'] = df.apply(lambda x : 'Yes' if re.search(r'(\d+)ml', x['Us']).group(1) == re.search(r'(\d+)ml', x['Competitor']).group(1) else "No", axis = 1)
Result:
Doesn't matter whether 'ml' is in the end of in the middle of the string.

Parsing dbus monitor output messages

I am trying to parse the dbus monitor output messages. It has most of the messages as multi-line entries(including parameters). I need to parse and concatenate individual log messages to a single line entry.
The dbus-monitor output messages appear as below,
method call time=462.117843 sender=:1.62 -> destination=org.freedesktop.filehandler serial=122 path=/org/freedesktop/filehandler/routing; interface=org.freedesktop.filehandler.routing; member=start
int16 29877
uint16 0
method return time=462.117844 sender=org.freedesktop.filehandler -> destination=:1.62 serial=2210 reply_serial=122
int16 29877
uint16 0
method call time=462.117845 sender=:1.62 -> destination=org.freedesktop.filehandler serial=123 path=/org/freedesktop/filehandler/routing; interface=org.freedesktop.filehandler.routing; member=comment
string "starting .."
string "routing"
method return time=462.117846 sender=:1.19 -> destination=:1.62 serial=2212 reply_serial=123
int12 -23145
signal time=463.11223 sender=:1.64 -> destination=(null destination) serial=124 path=/org/freedesktop/fileserver; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged
string "com.freedesktop.Systemserver"
array[
dict entry(
string "SystemTime"
variant struct{
byte 12
byte 9
byte 0
}
)
]
array [
]
This is the regex I tried to group the dbus messages(Parameter not grouped),
\b(signal|method call|method return)\b time=([\d,.]*) sender=([\w,.,:,(,), ]*) -> destination=([\w,.,:,(,), ]*) serial=([(,),\w]*) (?:path=([\w,\/]*); interface=([\w,.]*); member=([\w,_,-]*))?(?:reply_serial=([\d]*))?
I expect the output in the below format,
C [sender,serial] path interface+member (parameter1, parameter2, ...)
R [destination,reply_serial] interface+member (parameter1, parameter2, ...)
S [sender, serial] path interface+member (parameter1, parameter2, ...)
A sample output for the above dbus-monitor messages is shown below,
C [:1.62,122] /org/freedesktop/filehandler/routing org.freedesktop.filehandler.routing.start (29877,0)
R [:1.62,122] org.freedesktop.filehandler.routing.start (29877,0)
C [:1.62,123] /org/freedesktop/filehandler/routing org.freedesktop.filehandler.routing.comment ("starting", "routing")
R [:1.62,123] org.freedesktop.filehandler.routing.comment (-23145)
S [:1.64, 124] /org/freedesktop/fileserver org.freedesktop.DBus.Properties.PropertiesChanged ("com.freedesktop.Systemserver"[("SystemTime",{12,9,0})][])
How can the above expected result be achieved when the entries are usually multi-line? Also, the SIGNALS has multiple encapsulations making it difficult to access the parameters. Can someone help with the parsing of these dbus messages to the expected format?
Can you suggest how the code can be rewritten to process line by line?
Here I rearranged it accordingly:
import re
import sys
regex = r'\b(signal|method call|method return)\b time=([\d,.]*) sender=([\w,.,:,(,), ]*) -> destination=([\w,.,:,(,), ]*) serial=([(,),\w]*) (?:path=([\w,\/]*); interface=([\w,.]*); member=([\w,_,-]*))?(?:reply_serial=([\d]*))?'
remember = dict()
sep = None
for line in open('dbusl.in'):
m = re.match(regex, line)
if m:
if sep is not None: print ")" # end the previous parameter group
m = list(m.groups()) # each match is 9 capturing groups
if m[0] == 'method call':
print "C [{2},{4}] {5} {6}.{7}".format(*m),
remember[m[4]] = m[6:8] # store interface+member for return
if m[0] == 'method return':
m[6:8] = remember.pop(m[8]) # recall stored interface+member
print "R [{3},{8}] {6}.{7}".format(*m),
if m[0] == 'signal':
print "S [{2}, {4}] {5} {6}.{7}".format(*m),
sep = "("
else:
p = line.rstrip() # now handle parameters
if p[-1] in "[](){}": # with "encapsulations":
p = p[-1] # delete spaces, "array", "dict ..."
p = re.sub('^\s*\w*\s*', '', p) # delete spaces and data type
if p[-1] in "])}":
sep = '' # no separator before closing
print sep+p,
sys.stdout.softspace=0
if p[-1] in "[](){}": sep = ''
else: sep = ', ' # separator after data item
print ")" # end the previous parameter group
Note that I also changed m[6:8] = remember[m[8]] to m[6:8] = remember.pop(m[8]) in order to free the memory of no longer needed interface+member data.
If you absolutely have to use dbus-monitor, it’s probably best to use its PCAP output mode by passing the --pcap option to it. That outputs in a well-documented structured format which can be read by libpcap.
As you already have a usable regex, you can build on it by using it with re.split to get the needed message parts. Note that this yields a separate string for each capture group plus one string with the parameters, for each message entry. This example assumes that all the messages are in the string messages:
import re
import sys
regex = r'\b(signal|method call|method return)\b time=([\d,.]*) sender=([\w,.,:,(,), ]*) -> destination=([\w,.,:,(,), ]*) serial=([(,),\w]*) (?:path=([\w,\/]*); interface=([\w,.]*); member=([\w,_,-]*))?(?:reply_serial=([\d]*))?'
m = re.split(regex, messages)
m = m[1:] # discard empty? text before first match
remember = dict()
while m: # each match group is 9 capturing groups + 1 parameter group
if m[0] == 'method call':
print "C [{2},{4}] {5} {6}.{7}".format(*m),
remember[m[4]] = m[6:8] # store interface+member for return
if m[0] == 'method return':
m[6:8] = remember[m[8]] # recall stored interface+member
print "R [{3},{8}] {6}.{7}".format(*m),
if m[0] == 'signal':
print "S [{2}, {4}] {5} {6}.{7}".format(*m),
# now handle parameters
sep = "("
for p in m[9].split('\n')[1:-1]: # except empty string at start and end
if p[-1] in "[](){}": # with "encapsulations":
p = p[-1] # delete spaces, "array", "dict ..."
p = re.sub('^\s*\w*\s*', '', p) # delete spaces and data type
if p[-1] in "])}":
sep = '' # no separator before closing
print sep+p,
sys.stdout.softspace=0
if p[-1] in "[](){}": sep = ''
else: sep = ', ' # separator after data item
print ")"
m = m[10:] # delete the processed match group of 10
The output with your sample data is:
C [:1.62,122] /org/freedesktop/filehandler/routing org.freedesktop.filehandler.routing.start (29877, 0)
R [:1.62,122] org.freedesktop.filehandler.routing.start (29877, 0)
C [:1.62,123] /org/freedesktop/filehandler/routing org.freedesktop.filehandler.routing.comment ("starting ..", "routing")
R [:1.62,123] org.freedesktop.filehandler.routing.comment (-23145)
S [:1.64, 124] /org/freedesktop/fileserver org.freedesktop.DBus.Properties.PropertiesChanged ("com.freedesktop.Systemserver", [("SystemTime", {12, 9, 0})][])

Extract Entire String Right of the Delimiter from a multi-line string after a match

From the multi-line string, I am attempting to extract the entire string right of = sign after a match. However, only portion of the string is extracted.
How can I rectify this problem? I am open to other implementations search/extraction operations as well.
import re
s = '''jaguar.vintage.aircards = 2
jaguar.vintage.hw.sdb.size = 512.1 GB
jaguar.vintage.hw.tm.firmware = SWI9X15C_05.05.16.02 r21040 carmd-fwbuild1 2014/03/17 23:49:48
jaguar.vintage.hw.tm.hardware = 1.0
jaguar.vintage.hw.tm.iccid = 8901260591783960689
jaguar.vintage.hw.tm.imei = 359225051166726
jaguar.vintage.hw.tm.imsi = 310260598396068
jaguar.vintage.hw.tm.model = MC7354
jaguar.vintage.hw.wifi1.mac = 00:30:1a:4e:06:7a
jaguar.vintage.hw.wifi2.mac = 00:30:1a:4e:06:79
jaguar.vintage.part = P34110-002
jaguar.vintage.product = P34101
jaguar.vintage.psoc = 0.1.16
jaguar.vintage.serial = 34110002T0021
jaguar.vintage.slavepsoc1 = 0.1.5
jaguar.vintage.sw.app.release = 4.0.0.41387-201902131138git367fbda8e
'''
# print(s)
# release = (s.split('jaguar.vintage.sw.app.release =')[1]).strip()
# print(release)
#part_number = jaguar.vintage.part = P34110-002
pnumsrch = r"jaguar.vintage.part =.*?(?=\w)(\w+)"
part_number = re.findall(pnumsrch, s)
print(part_number[0])
# release_number = jaguar.vintage.sw.app.release = 4.0.0.41387-201902131138git367fbda8e
relnumsrch = r"jaguar.vintage.sw.app.release =.*?(?=\w)(\w+)"
rel_number = re.findall(relnumsrch, s)
print(rel_number[0])
Actual:
P34110
4
Expected:
P34110-002
4.0.0.41387-201902131138git367fbda8e
Since . does not match a newline character by default, you can simply use .* to match the rest of the line:
pnumsrch = r"jaguar.vintage.part = (.*)"
and:
relnumsrch = r"jaguar.vintage.sw.app.release = (.*)"
Just catch everything that's not a new line Demo:
pat = re.compile(r'jaguar\.vintage\.part = ([^\n]+)')
pat2 = re.compile(r'jaguar\.vintage\.sw\.app\.release = ([^\n]+)')
>>> pat.findall(s)
['P34110-002']
>>> pat2.findall(s)
['4.0.0.41387-201902131138git367fbda8e']
You also should escape your periods in your pattern.
As mentioned by #WiktorStribiżew, just . is good enough for the [^\n] portion:
pat = re.compile(r'jaguar\.vintage\.part = (.+)')
pat2 = re.compile(r'jaguar\.vintage\.sw\.app\.release = (.+)')

Python truncate a long string

How does one truncate a string to 75 characters in Python?
This is how it is done in JavaScript:
var data="saddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddsaddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddsadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
var info = (data.length > 75) ? data.substring[0,75] + '..' : data;
info = (data[:75] + '..') if len(data) > 75 else data
Even more concise:
data = data[:75]
If it is less than 75 characters there will be no change.
Even shorter :
info = data[:75] + (data[75:] and '..')
If you are using Python 3.4+, you can use textwrap.shorten from the standard library:
Collapse and truncate the given text to fit in the given width.
First the whitespace in text is collapsed (all whitespace is replaced
by single spaces). If the result fits in the width, it is returned.
Otherwise, enough words are dropped from the end so that the remaining
words plus the placeholder fit within width:
>>> textwrap.shorten("Hello world!", width=12)
'Hello world!'
>>> textwrap.shorten("Hello world!", width=11)
'Hello [...]'
>>> textwrap.shorten("Hello world", width=10, placeholder="...")
'Hello...'
For a Django solution (which has not been mentioned in the question):
from django.utils.text import Truncator
value = Truncator(value).chars(75)
Have a look at Truncator's source code to appreciate the problem:
https://github.com/django/django/blob/master/django/utils/text.py#L66
Concerning truncation with Django:
Django HTML truncation
With regex:
re.sub(r'^(.{75}).*$', '\g<1>...', data)
Long strings are truncated:
>>> data="11111111112222222222333333333344444444445555555555666666666677777777778888888888"
>>> re.sub(r'^(.{75}).*$', '\g<1>...', data)
'111111111122222222223333333333444444444455555555556666666666777777777788888...'
Shorter strings never get truncated:
>>> data="11111111112222222222333333"
>>> re.sub(r'^(.{75}).*$', '\g<1>...', data)
'11111111112222222222333333'
This way, you can also "cut" the middle part of the string, which is nicer in some cases:
re.sub(r'^(.{5}).*(.{5})$', '\g<1>...\g<2>', data)
>>> data="11111111112222222222333333333344444444445555555555666666666677777777778888888888"
>>> re.sub(r'^(.{5}).*(.{5})$', '\g<1>...\g<2>', data)
'11111...88888'
limit = 75
info = data[:limit] + '..' * (len(data) > limit)
This method doesn't use any if:
data[:75] + bool(data[75:]) * '..'
This just in:
n = 8
s = '123'
print s[:n-3] + (s[n-3:], '...')[len(s) > n]
s = '12345678'
print s[:n-3] + (s[n-3:], '...')[len(s) > n]
s = '123456789'
print s[:n-3] + (s[n-3:], '...')[len(s) > n]
s = '123456789012345'
print s[:n-3] + (s[n-3:], '...')[len(s) > n]
123
12345678
12345...
12345...
info = data[:75] + ('..' if len(data) > 75 else '')
info = data[:min(len(data), 75)
You can't actually "truncate" a Python string like you can do a dynamically allocated C string. Strings in Python are immutable. What you can do is slice a string as described in other answers, yielding a new string containing only the characters defined by the slice offsets and step.
In some (non-practical) cases this can be a little annoying, such as when you choose Python as your interview language and the interviewer asks you to remove duplicate characters from a string in-place. Doh.
Yet another solution. With True and False you get a little feedback about the test at the end.
data = {True: data[:75] + '..', False: data}[len(data) > 75]
Coming very late to the party I want to add my solution to trim text at character level that also handles whitespaces properly.
def trim_string(s: str, limit: int, ellipsis='…') -> str:
s = s.strip()
if len(s) > limit:
return s[:limit-1].strip() + ellipsis
return s
Simple, but it will make sure you that hello world with limit=6 will not result in an ugly hello … but hello… instead.
It also removes leading and trailing whitespaces, but not spaces inside. If you also want to remove spaces inside, checkout this stackoverflow post
>>> info = lambda data: len(data)>10 and data[:10]+'...' or data
>>> info('sdfsdfsdfsdfsdfsdfsdfsdfsdfsdfsdf')
'sdfsdfsdfs...'
>>> info('sdfsdf')
'sdfsdf'
>>>
Simple and short helper function:
def truncate_string(value, max_length=255, suffix='...'):
string_value = str(value)
string_truncated = string_value[:min(len(string_value), (max_length - len(suffix)))]
suffix = (suffix if len(string_value) > max_length else '')
return string_truncated+suffix
Usage examples:
# Example 1 (default):
long_string = ""
for number in range(1, 1000):
long_string += str(number) + ','
result = truncate_string(long_string)
print(result)
# Example 2 (custom length):
short_string = 'Hello world'
result = truncate_string(short_string, 8)
print(result) # > Hello...
# Example 3 (not truncated):
short_string = 'Hello world'
result = truncate_string(short_string)
print(result) # > Hello world
If you wish to do some more sophisticated string truncate you can adopt sklearn approach as implement by:
sklearn.base.BaseEstimator.__repr__
(See Original full code at: https://github.com/scikit-learn/scikit-learn/blob/f3f51f9b6/sklearn/base.py#L262)
It adds benefits such as avoiding truncate in the middle of the word.
def truncate_string(data, N_CHAR_MAX=70):
# N_CHAR_MAX is the (approximate) maximum number of non-blank
# characters to render. We pass it as an optional parameter to ease
# the tests.
lim = N_CHAR_MAX // 2 # apprx number of chars to keep on both ends
regex = r"^(\s*\S){%d}" % lim
# The regex '^(\s*\S){%d}' % n
# matches from the start of the string until the nth non-blank
# character:
# - ^ matches the start of string
# - (pattern){n} matches n repetitions of pattern
# - \s*\S matches a non-blank char following zero or more blanks
left_lim = re.match(regex, data).end()
right_lim = re.match(regex, data[::-1]).end()
if "\n" in data[left_lim:-right_lim]:
# The left side and right side aren't on the same line.
# To avoid weird cuts, e.g.:
# categoric...ore',
# we need to start the right side with an appropriate newline
# character so that it renders properly as:
# categoric...
# handle_unknown='ignore',
# so we add [^\n]*\n which matches until the next \n
regex += r"[^\n]*\n"
right_lim = re.match(regex, data[::-1]).end()
ellipsis = "..."
if left_lim + len(ellipsis) < len(data) - right_lim:
# Only add ellipsis if it results in a shorter repr
data = data[:left_lim] + "..." + data[-right_lim:]
return data
There's no need for a regular expression but you do want to use string formatting rather than the string concatenation in the accepted answer.
This is probably the most canonical, Pythonic way to truncate the string data at 75 characters.
>>> data = "saddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddsaddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddsadddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
>>> info = "{}..".format(data[:75]) if len(data) > 75 else data
>>> info
'111111111122222222223333333333444444444455555555556666666666777777777788888...'
Here's a function I made as part of a new String class... It allows adding a suffix ( if the string is size after trimming and adding it is long enough - although you don't need to force the absolute size )
I was in the process of changing a few things around so there are some useless logic costs ( if _truncate ... for instance ) where it is no longer necessary and there is a return at the top...
But, it is still a good function for truncating data...
##
## Truncate characters of a string after _len'nth char, if necessary... If _len is less than 0, don't truncate anything... Note: If you attach a suffix, and you enable absolute max length then the suffix length is subtracted from max length... Note: If the suffix length is longer than the output then no suffix is used...
##
## Usage: Where _text = 'Testing', _width = 4
## _data = String.Truncate( _text, _width ) == Test
## _data = String.Truncate( _text, _width, '..', True ) == Te..
##
## Equivalent Alternates: Where _text = 'Testing', _width = 4
## _data = String.SubStr( _text, 0, _width ) == Test
## _data = _text[ : _width ] == Test
## _data = ( _text )[ : _width ] == Test
##
def Truncate( _text, _max_len = -1, _suffix = False, _absolute_max_len = True ):
## Length of the string we are considering for truncation
_len = len( _text )
## Whether or not we have to truncate
_truncate = ( False, True )[ _len > _max_len ]
## Note: If we don't need to truncate, there's no point in proceeding...
if ( not _truncate ):
return _text
## The suffix in string form
_suffix_str = ( '', str( _suffix ) )[ _truncate and _suffix != False ]
## The suffix length
_len_suffix = len( _suffix_str )
## Whether or not we add the suffix
_add_suffix = ( False, True )[ _truncate and _suffix != False and _max_len > _len_suffix ]
## Suffix Offset
_suffix_offset = _max_len - _len_suffix
_suffix_offset = ( _max_len, _suffix_offset )[ _add_suffix and _absolute_max_len != False and _suffix_offset > 0 ]
## The truncate point.... If not necessary, then length of string.. If necessary then the max length with or without subtracting the suffix length... Note: It may be easier ( less logic cost ) to simply add the suffix to the calculated point, then truncate - if point is negative then the suffix will be destroyed anyway.
## If we don't need to truncate, then the length is the length of the string.. If we do need to truncate, then the length depends on whether we add the suffix and offset the length of the suffix or not...
_len_truncate = ( _len, _max_len )[ _truncate ]
_len_truncate = ( _len_truncate, _max_len )[ _len_truncate <= _max_len ]
## If we add the suffix, add it... Suffix won't be added if the suffix is the same length as the text being output...
if ( _add_suffix ):
_text = _text[ 0 : _suffix_offset ] + _suffix_str + _text[ _suffix_offset: ]
## Return the text after truncating...
return _text[ : _len_truncate ]
Suppose that stryng is a string which we wish to truncate and that nchars is the number of characters desired in the output string.
stryng = "sadddddddddddddddddddddddddddddddddddddddddddddddddd"
nchars = 10
We can truncate the string as follows:
def truncate(stryng:str, nchars:int):
return (stryng[:nchars - 6] + " [...]")[:min(len(stryng), nchars)]
The results for certain test cases are shown below:
s = "sadddddddddddddddddddddddddddddd!"
s = "sa" + 30*"d" + "!"
truncate(s, 2) == sa
truncate(s, 4) == sadd
truncate(s, 10) == sadd [...]
truncate(s, len(s)//2) == sadddddddd [...]
My solution produces reasonable results for the test cases above.
However, some pathological cases are shown below:
Some Pathological Cases!
truncate(s, len(s) - 3)() == sadddddddddddddddddddddd [...]
truncate(s, len(s) - 2)() == saddddddddddddddddddddddd [...]
truncate(s, len(s) - 1)() == sadddddddddddddddddddddddd [...]
truncate(s, len(s) + 0)() == saddddddddddddddddddddddddd [...]
truncate(s, len(s) + 1)() == sadddddddddddddddddddddddddd [...
truncate(s, len(s) + 2)() == saddddddddddddddddddddddddddd [..
truncate(s, len(s) + 3)() == sadddddddddddddddddddddddddddd [.
truncate(s, len(s) + 4)() == saddddddddddddddddddddddddddddd [
truncate(s, len(s) + 5)() == sadddddddddddddddddddddddddddddd
truncate(s, len(s) + 6)() == sadddddddddddddddddddddddddddddd!
truncate(s, len(s) + 7)() == sadddddddddddddddddddddddddddddd!
truncate(s, 9999)() == sadddddddddddddddddddddddddddddd!
Notably,
When the string contains new-line characters (\n) there could be an issue.
When nchars > len(s) we should print string s without trying to print the "[...]"
Below is some more code:
import io
class truncate:
"""
Example of Code Which Uses truncate:
```
s = "\r<class\n 'builtin_function_or_method'>"
s = truncate(s, 10)()
print(s)
```
Examples of Inputs and Outputs:
truncate(s, 2)() == \r
truncate(s, 4)() == \r<c
truncate(s, 10)() == \r<c [...]
truncate(s, 20)() == \r<class\n 'bu [...]
truncate(s, 999)() == \r<class\n 'builtin_function_or_method'>
```
Other Notes:
Returns a modified copy of string input
Does not modify the original string
"""
def __init__(self, x_stryng: str, x_nchars: int) -> str:
"""
This initializer mostly exists to sanitize function inputs
"""
try:
stryng = repr("".join(str(ch) for ch in x_stryng))[1:-1]
nchars = int(str(x_nchars))
except BaseException as exc:
invalid_stryng = str(x_stryng)
invalid_stryng_truncated = repr(type(self)(invalid_stryng, 20)())
invalid_x_nchars = str(x_nchars)
invalid_x_nchars_truncated = repr(type(self)(invalid_x_nchars, 20)())
strm = io.StringIO()
print("Invalid Function Inputs", file=strm)
print(type(self).__name__, "(",
invalid_stryng_truncated,
", ",
invalid_x_nchars_truncated, ")", sep="", file=strm)
msg = strm.getvalue()
raise ValueError(msg) from None
self._stryng = stryng
self._nchars = nchars
def __call__(self) -> str:
stryng = self._stryng
nchars = self._nchars
return (stryng[:nchars - 6] + " [...]")[:min(len(stryng), nchars)]
Here's a simple function that will truncate a given string from either side:
def truncate(string, length=75, beginning=True, insert='..'):
'''Shorten the given string to the given length.
An ellipsis will be added to the section trimmed.
:Parameters:
length (int) = The maximum allowed length before trunicating.
beginning (bool) = Trim starting chars, else; ending.
insert (str) = Chars to add at the trimmed area. (default: ellipsis)
:Return:
(str)
ex. call: truncate('12345678', 4)
returns: '..5678'
'''
if len(string)>length:
if beginning: #trim starting chars.
string = insert+string[-length:]
else: #trim ending chars.
string = string[:length]+insert
return string
Here I use textwrap.shorten and handle more edge cases. also include part of the last word in case this word is more than 50% of the max width.
import textwrap
def shorten(text: str, width=30, placeholder="..."):
"""Collapse and truncate the given text to fit in the given width.
The text first has its whitespace collapsed. If it then fits in the *width*, it is returned as is.
Otherwise, as many words as possible are joined and then the placeholder is appended.
"""
if not text or not isinstance(text, str):
return str(text)
t = text.strip()
if len(t) <= width:
return t
# textwrap.shorten also throws ValueError if placeholder too large for max width
shorten_words = textwrap.shorten(t, width=width, placeholder=placeholder)
# textwrap.shorten doesn't split words, so if the text contains a long word without spaces, the result may be too short without this word.
# Here we use a different way to include the start of this word in case shorten_words is less than 50% of `width`
if len(shorten_words) - len(placeholder) < (width - len(placeholder)) * 0.5:
return t[:width - len(placeholder)].strip() + placeholder
return shorten_words
Tests:
>>> shorten("123 456", width=7, placeholder="...")
'123 456'
>>> shorten("1 23 45 678 9", width=12, placeholder="...")
'1 23 45...'
>>> shorten("1 23 45 678 9", width=10, placeholder="...")
'1 23 45...'
>>> shorten("01 23456789", width=10, placeholder="...")
'01 2345...'
>>> shorten("012 3 45678901234567", width=17, placeholder="...")
'012 3 45678901...'
>>> shorten("1 23 45 678 9", width=9, placeholder="...")
'1 23...'
>>> shorten("1 23456", width=5, placeholder="...")
'1...'
>>> shorten("123 456", width=5, placeholder="...")
'12...'
>>> shorten("123 456", width=6, placeholder="...")
'123...'
>>> shorten("12 3456789", width=9, placeholder="...")
'12 345...'
>>> shorten(" 12 3456789 ", width=9, placeholder="...")
'12 345...'
>>> shorten('123 45', width=4, placeholder="...")
'1...'
>>> shorten('123 45', width=3, placeholder="...")
'...'
>>> shorten("123456", width=3, placeholder="...")
'...'
>>> shorten([1], width=9, placeholder="...")
'[1]'
>>> shorten(None, width=5, placeholder="...")
'None'
>>> shorten("", width=9, placeholder="...")
''

Categories