1#!/usr/bin/env python 2from __future__ import division, print_function 3"""String utilities, with an emphasis on support for sexagesimal numbers 4(e.g. degrees:minutes:seconds). 5 6To do: 7- Add tests for dmsStrFromDeg with omitExtraFields true, 8 dmsStrFromSec, quoteStr etc. 9 10History: 112001-03-12 ROwen strToFloat modified to handle ".". 122001-03-14 ROwen Added prettyDict; still need to add a test for it. 132001-04-13 ROwen Fixed a bug in splitDMSStr: -0:... was not retaining the minus sign; 14 added dmsStrToSec, which may be useful for handling durations; 15 modified splitDMSStr to allow an arbitrary number digits in the first field 16 (use range checking of the equivalent numeric value to limit this), 17 to return sing as a separate entity, and to return all strings; 18 improved the test suite, including adding a nearly silent test. 192001-04-20 ROwen Fixed a bug in degToDMSStr: borderline cases could print 20 as 60 instead of 00 and incrementing the next field. 212001-07-22 ROwen Made DegStr work on unix as well as Mac 22 (I still don't know the Windows character). 232001-08-29 ROwen Made DegStr and DMSStr unicode strings, a reasonable choice 24 now that one can print unicode characters. 252001-11-01 ROwen Added nFields argument to degToDMSStr and secToDMSStr; 26 changed degToDMSStr, secToDMSStr and neatenDMSStr to eliminate leading spaces. 272002-08-08 ROwen Moved to RO and renamed from ROStringUtil. Renamed 28 xToY functions to yFromX. Hid private constants with leading underscore. 292002-08-21 ROwen Modified splitDMSStr to require all but the first field be <60. 302003-04-04 ROwen Bug fix: dmsStrFromDeg and dmsStrFromSec mis-handled 31 some negative values. 322003-06-19 ROwen Fixed two tests which broke in upgrades listed above. 332003-07-15 ROwen Added omitExtraFields to degFromDMSStr and secFromDMSStr; 34 added quoteStr function. 352003-10-27 ROwen Added plural function. 362004-01-09 ROwen Added AngstromStr, LambdaStr and MuStr constants. 372004-05-18 ROwen Stopped importing sys since it was not being used. 382005-06-27 ROwen Fixed a nonfunctional assert statement in splitDMSStr. 392006-01-09 ROwen Fixed a bug in dmsStrFromDeg: dmsStrFromDeg(-50.650002) = "-50:38:60.0" 40 and a related bug in dmsStrFroMSec. Thanks to John Lucey. 41 (Note: the test code had a case for it, but expected the wrong value.) 422007-06-04 ROwen Bug fix: dmsStrFromSec gave bad results if nFields != 3. 432008-01-30 ROwen Removed unused variable signNum (found by pychecker). 442008-04-29 ROwen Added strFromException, a unicode-safe replacement for str(exception). 452008-05-02 ROwen Made prettyDict unicode-safe by using repr. 462008-11-14 ROwen Added unquoteStr. 472014-05-07 ROwen Changed is str test to use basestring. 482014-09-17 ROwen Modified to test for Exception instead of StandardError 492015-01-06 ROwen Improved dmsStrFromDeg and dmsStrFromSec to handle non-finite values. 502015-09-24 ROwen Replace "== None" with "is None" to modernize the code. 512015-11-03 ROwen Replace "!= None" with "is not None" to modernize the code. 522015-11-05 ROwen Stop using dangerous bare "except:". 53""" 54import re 55import numpy 56 57AngstromStr = u"\N{ANGSTROM SIGN}" 58DegStr = u"\N{DEGREE SIGN}" 59DMSStr = DegStr + u"'\"" 60LambdaStr = u"\u00c5" # for some reason this fails: u"\N{GREEK SMALL LETTER LAMBDA}" 61MuStr = u"\N{GREEK SMALL LETTER MU}" 62 63def dmsStrFromDeg (decDeg, nFields=3, precision=1, omitExtraFields = False): 64 """Convert a number to a sexagesimal string with 1-3 fields. 65 66 Inputs: 67 - decDeg: value in decimal degrees or hours 68 - nFields: number of fields; <=1 for dddd.ddd, 2 for dddd:mm.mmm, >=3 for dddd:mm:ss.sss 69 - precision: number of digits after the decimal point in the last field; 70 if 0, no decimal point is printed; must be >= 0 71 - omitExtraFields: omit fields that are zero, starting from the right 72 73 Error conditions: 74 - Raises ValueError if precision < 0 75 - Returns "" if decDeg is not finite 76 """ 77 if not numpy.isfinite(decDeg): 78 return "" 79 nFields = min(3, nFields) 80 signStr, fieldStrs = _getDMSFields(decDeg, nFields, precision) 81 82 if omitExtraFields: 83 while fieldStrs and float(fieldStrs[-1]) == 0.0: 84 fieldStrs.pop(-1) 85 86 return signStr + ":".join(fieldStrs) 87 88def dmsStrFromSec (decSec, nFields=3, precision=1, omitExtraFields = True): 89 """Convert a number, in seconds, to a sexagesimal string. 90 Similar to dmsStrFromDeg, but takes seconds, not degrees, 91 and omitExtraFields omits fields from the left, not the right. 92 93 Inputs: 94 - decSec: value in decimal seconds or arc seconds 95 - nFields: number of fields; <=1 for ss.sss, 2 for mm:ss.ss, >= 3 for dddd:mm:ss.sss 96 - precision: number of digits after the decimal point in the seconds field; 97 if 0, no decimal point is printed; must be >= 0 98 - omitExtraFields: omit fields that are zero, starting from the left. 99 100 Error conditions: 101 - Raises ValueError if precision < 0 102 - Returns "" if decDeg is not finite 103 """ 104 if not numpy.isfinite(decSec): 105 return "" 106 nFields = min(3, nFields) 107 if nFields < 1: 108 raise ValueError("nFields=%r; must be >= 1" % (nFields,)) 109 adjNum = decSec / (60.0**(nFields-1)) 110 signStr, fieldStrs = _getDMSFields(adjNum, nFields, precision) 111 112 if omitExtraFields: 113 while fieldStrs and float(fieldStrs[0]) == 0.0: 114 fieldStrs.pop(0) 115 116 return signStr + ":".join(fieldStrs) 117 118def degFromDMSStr (dmsStr): 119 """Convert a string of the basic form dd[:mm[:ss]] to decimal degrees. 120 See splitDMSStr for details of the format. 121 122 Error conditions: 123 - Raises ValueError if the string cannot be parsed 124 """ 125 dmsItems = splitDMSStr(dmsStr) 126 127 # extract sign 128 if dmsItems[0] == '-': 129 signMult = -1.0 130 else: 131 signMult = 1.0 132 dmsItems[0:1] = [] 133 134 # combine last two elements and convert to float 135 dmsItems[-2:] = [floatFromStr(dmsItems[-2]) + floatFromStr(dmsItems[-1])] 136 137 # convert all but last item to float 138 for ind in range(len(dmsItems) - 1): 139 dmsItems[ind] = intFromStr(dmsItems[ind]) 140 141 dmsItems.reverse() 142 decDeg = 0.0 143 for dmsField in dmsItems: 144 decDeg = abs(dmsField) + (decDeg / 60.0) 145 return signMult * decDeg 146 147def secFromDMSStr (dmsStr): 148 """Convert a string of the basic form [[dd:]mm:]ss to decimal degrees. 149 Note that missing fields are handled differently than degFromDMSStr! 150 See splitDMSStr for details of the format. 151 152 error conditions: 153 raises ValueError if the string cannot be parsed 154 """ 155 dmsItems = splitDMSStr(dmsStr) 156 157 # extract sign 158 if dmsItems[0] == '-': 159 signMult = -1.0 160 else: 161 signMult = 1.0 162 dmsItems[0:1] = [] 163 164 # combine last two elements and convert to float 165 dmsItems[-2:] = [floatFromStr(dmsItems[-2]) + floatFromStr(dmsItems[-1])] 166 167 # convert all but last item to float 168 for ind in range(len(dmsItems) - 1): 169 dmsItems[ind] = intFromStr(dmsItems[ind]) 170 171 decSec = 0.0 172 for dmsField in dmsItems: 173 decSec = abs(dmsField) + (decSec * 60.0) 174 return signMult * decSec 175 176def secStrFromDMSStr(dmsStr): 177 """Convert a string of the basic form [[dd:]mm:]ss to decimal seconds 178 preserving the original accuracy of seconds 179 Note that missing fields are handled differently than degFromDMSStr! 180 See splitDMSStr for details of the format. 181 182 error conditions: 183 raises ValueError if the string cannot be parsed 184 """ 185 dmsItems = splitDMSStr(dmsStr) 186 187 # extract sign and fractional seconds (includes decimal point) 188 signStr = dmsItems[0] 189 fracSecStr = dmsItems[-1] 190 191 # compute integer seconds 192 # convert all but first and last items to integers 193 intList = [intFromStr(item) for item in dmsItems[1:-1]] 194 195 intSec = 0 196 for intVal in intList: 197 intSec = abs(intVal) + (intSec * 60) 198 return "%s%s%s" % (signStr, intSec, fracSecStr) 199 200FloatChars = "0123456789+-.eE" 201 202def checkDMSStr(dmsStr): 203 """Verify a sexagesimal string; returns True if valid, False if not 204 """ 205 try: 206 splitDMSStr(dmsStr) 207 return True 208 except Exception: 209 return False 210 211def dmsStrFieldsPrec(dmsStr): 212 """Return the following information about a sexagesimal string: 213 - the number of colon-separated fields 214 - the precision of the right-most field 215 """ 216 if dmsStr == "": 217 return (0, 0) 218 219 precArry = dmsStr.split(".") 220 if len(precArry) > 1: 221 precision = len(precArry[1]) 222 else: 223 precision = 0 224 nFields = dmsStr.count(":") + 1 225 return (nFields, precision) 226 227def findLeftNumber(astr, ind): 228 """Find the starting and ending index of the number 229 enclosing or to the left of index "ind". 230 Return (None, None) if no number found. 231 232 Warning: this is not a sophisticated routine. It looks for 233 the a run of characters that could be present 234 in a floating point number. It does not sanity checking 235 to see if they make a valid number. 236 """ 237 leftInd = _findLeftOfLeftNumber(astr, ind) 238 if leftInd is None: 239 return (None, None) 240 rightInd = _findRightOfRightNumber(astr, leftInd) 241 if rightInd is None: 242 return (None, None) 243 return (leftInd, rightInd) 244 245def findRightNumber(astr, ind): 246 """Find the starting and ending index of the number 247 enclosing or to the right of index "ind". 248 Returns (None, None) if no number found. 249 250 Warning: this is not a sophisticated routine. It looks for 251 the a run of characters that could be present 252 in a floating point number. It does not sanity checking 253 to see if they make a valid number. 254 """ 255 rightInd = _findRightOfRightNumber(astr, ind) 256 if rightInd is None: 257 return (None, None) 258 leftInd = _findLeftOfLeftNumber(astr, rightInd) 259 if leftInd is None: 260 return (None, None) 261 return (leftInd, rightInd) 262 263def _findLeftOfLeftNumber(astr, ind): 264 """Find the index of the first character of the number 265 enclosing or to the left of index "ind". 266 Returns None if no number found. 267 268 Warning: this is not a sophisticated routine. It looks for 269 the left-most of a run of characters that could be present 270 in a floating point number. It does not sanity checking 271 to see if they make a valid number. 272 """ 273 leftInd = None 274 for tryind in range(ind, -1, -1): 275 if astr[tryind] in FloatChars: 276 leftInd = tryind 277 elif leftInd is not None: 278 break 279 return leftInd 280 281def _findRightOfRightNumber(astr, ind): 282 """Find the index of the last character of the number 283 enclosing or to the right of index "ind". 284 Returns None if no number found. 285 286 Warning: this is not a sophisticated routine. It looks for 287 the right-most of a run of characters that could be present 288 in a floating point number. It does not sanity checking 289 to see if they make a valid number. 290 """ 291 rightInd = None 292 for tryind in range(ind, len(astr)): 293 if astr[tryind] in FloatChars: 294 rightInd = tryind 295 elif rightInd is not None: 296 break 297 return rightInd 298 299def neatenDMSStr (dmsStr): 300 """Convert a sexagesimal string to a neater version. 301 302 error conditions: 303 raises ValueError if the string cannot be parsed 304 """ 305 if dmsStr == "": 306 return "" 307 308 precArry = dmsStr.split(".") 309 if len(precArry) > 1: 310 precision = len(precArry[1]) 311 else: 312 precision = 0 313 fieldArry = dmsStr.split(":") 314 nFields = len(fieldArry) 315 316 floatValue = degFromDMSStr(dmsStr) 317 return dmsStrFromDeg(floatValue, nFields=nFields, precision=precision) 318 319def plural(num, singStr, plStr): 320 """Return singStr or plStr depending if num == 1 or not. 321 A minor convenience for formatting messages (in lieu of ?: notation) 322 """ 323 if num == 1: 324 return singStr 325 return plStr 326 327def prettyDict(aDict, entrySepStr = "\n", keyValSepStr = ": "): 328 """Format a dictionary in a nice way 329 330 Inputs: 331 aDict: the dictionary to pretty-print 332 entrySepStr: string separating each dictionary entry 333 keyValSepStr: string separating key and value for each entry 334 335 Returns a string containing the pretty-printed dictionary 336 """ 337 sortedKeys = aDict.keys() 338 sortedKeys.sort() 339 eltList = [] 340 for aKey in sortedKeys: 341 eltList.append(repr(aKey) + keyValSepStr + repr(aDict[aKey])) 342 return entrySepStr.join(eltList) 343 344# constants used by splitDMSStr 345# DMSRE = re.compile(r"^\s*([+-]?)(\d{0,3})\s*(?:\:\s*(\d{0,2})\s*){0,2}(\.\d*)?\s*$") 346_DegRE = re.compile(r"^\s*([+-]?)(\d*)(\.\d*)?\s*$") 347_DegMinRE = re.compile(r"^\s*([+-]?)(\d*)\s*\:\s*([0-5]?\d?)(\.\d*)?\s*$") 348_DegMinSecRE = re.compile(r"^\s*([+-]?)(\d*)\s*\:\s*([0-5]?\d?):\s*([0-5]?\d?)(\.\d*)?\s*$") 349 350def splitDMSStr (dmsStr): 351 """Split a sexagesimal string into fields 352 returns one of the following lists: 353 [sign, int deg, frac deg] 354 [sign, int deg, int min, frac min] 355 [sign, int deg, int min, int sec, frac sec] 356 where: 357 all values are strings 358 sign is one of ('', '+' or '-') 359 frac <whatever> includes a leading decimal point 360 361 error conditions: 362 raises ValueError if the string cannot be parsed 363 """ 364 assert isinstance(dmsStr, basestring) 365 m = _DegRE.match(dmsStr) or _DegMinRE.match(dmsStr) or _DegMinSecRE.match(dmsStr) 366 if m is None: 367 raise ValueError("splitDMSStr cannot parse %s as a sexagesimal string" % (dmsStr)) 368 matchSet = list(m.groups()) 369 if matchSet[-1] is None: 370 matchSet[-1] = '' 371 return matchSet 372 373_FloatRE = re.compile(r'^\s*[-+]?[0-9]*\.?[0-9]*(e[-+]?)?[0-9]*\s*$', re.IGNORECASE) 374_FloatNoExpRE = re.compile(r'^\s*[-+]?[0-9]*\.?[0-9]*\s*$') 375def floatFromStr(astr, allowExp=1): 376 """Convert a string representation of a number to a float; 377 unlike float(), partial representations (such as "", "-", "-.e") are taken as 0 378 and "nan" is forbidden. 379 380 error conditions: 381 raises ValueError if astr cannot be converted 382 """ 383 if allowExp: 384 match = _FloatRE.match(astr) 385 else: 386 match = _FloatNoExpRE.match(astr) 387 388 389 if match is None: 390 raise ValueError("cannot convert :%s: to a float" % (astr)) 391 392 try: 393 return float(astr) 394 except Exception: 395 # partial float 396 return 0.0 397 398_IntRE = re.compile(r'^\s*[-+]?[0-9]*\s*$') 399def intFromStr(astr): 400 """Convert a string representation of a number to an integer; 401 unlike int(), the blank string and "+" and "-" are treated as 0 402 403 error conditions: 404 raises ValueError if astr cannot be converted 405 """ 406 if _IntRE.match(astr) is None: 407 raise ValueError("cannot convert :%s: to an integer" % (astr)) 408 409 try: 410 return int(astr) 411 except Exception: 412 # partial int 413 return 0 414 415def quoteStr(astr, escChar='\\', quoteChar='"'): 416 """Escape all instances of quoteChar and escChar in astr 417 with a preceding escChar and surrounds the result with quoteChar. 418 419 Examples: 420 astr = 'foo" \bar' 421 quoteStr(astr) = '"foo\" \\bar"' 422 quoteStr(astr, escChar = '"') = '"foo"" \bar"' 423 424 This prepares a string for output. 425 """ 426 if escChar != quoteChar: 427 # escape escChar 428 astr = astr.replace(escChar, escChar + escChar) 429 # escape quoteChar and surround the result in quoteChar 430 return quoteChar + astr.replace(quoteChar, escChar + quoteChar) + quoteChar 431 432def unquoteStr(astr, escChar='\\', quoteChars='"\''): 433 """Remove quotes from a string and unescapes contained escaped quotes. 434 435 Based on email.unquote. 436 """ 437 if len(astr) > 1: 438 for quoteChar in quoteChars: 439 if astr.startswith(quoteChar) and astr.endswith(quoteChar): 440 return astr[1:-1].replace(escChar + escChar, escChar).replace(escChar + quoteChar, quoteChar) 441 return astr 442 443def strFromException(exc): 444 """Unicode-safe replacement for str(exception)""" 445 try: 446 return str(exc) 447 except Exception: 448 try: 449 return ",".join([unicode(s) for s in exc.args]) 450 except Exception: 451 # in case exc is some unexpected type 452 return repr(exc) 453 454def _getDMSFields (decDeg, nFields=3, precision=1): 455 """Return a string representation of dms fields for decDeg. 456 457 Inputs: 458 - decDeg: value in decimal degrees or hours 459 - nFields: number of fields; must be >= 1 (and >3 is probably never used) 460 - precision: number of digits after the decimal point in the last field; 461 if 0, no decimal point is printed; must be >= 0 462 463 Returns: 464 - signStr: "" or "-" 465 - fieldStrs: string value of each field (all positive) 466 467 To compute dms a a string, use: signStr + ":".join(fieldStrs) 468 This routine doesn't take that step to allow omitting fields 469 whose value is zero (e.g. see dmsStrFromDeg and dmsStrFromSec). 470 471 Error conditions: 472 - Raises ValueError if precision < 0 473 """ 474 if nFields < 1: 475 raise ValueError("nFields=%r; must be >= 1" % (nFields,)) 476 if precision < 0: 477 raise ValueError("precision=%r; must be >= 0" % (precision,)) 478 479 if decDeg < 0: 480 signStr = "-" 481 else: 482 signStr = "" 483 484 if nFields == 1: 485 retNum = round(abs(decDeg), precision) 486 retStr = "%.*f" % (precision, retNum) 487 return signStr, [retStr] 488 489 # compute a list of output fields; all but the last one are integer 490 remVal = abs(decDeg) 491 fieldNums = [] 492 for fieldNum in range(nFields-1): 493 (intVal, remVal) = divmod (abs(remVal), 1.0) 494 intVal = int(intVal) 495 fieldNums.append(intVal) 496 remVal *= 60.0 497 fieldNums.append(round(remVal, precision)) 498 499 # handle overflow 500 incrPrevField = False 501 for ind in range(nFields-1, -1, -1): 502 if incrPrevField: 503 fieldNums[ind] += 1 504 if (ind > 0) and (fieldNums[ind] >= 60): 505 fieldNums[ind] -= 60 506 incrPrevField = True 507 else: 508 incrPrevField = False 509 510 # compute fieldStrs 511 if precision > 0: 512 minFloatWidth = precision + 3 513 else: 514 minFloatWidth = 2 515 fieldStrs = ["%d" % (fieldNums[0],)] 516 for numVal in fieldNums[1:-1]: 517 fieldStrs.append("%02d" % (numVal,)) 518 fieldStrs.append("%0*.*f" % (minFloatWidth, precision, fieldNums[-1])) 519 520 return signStr, fieldStrs 521 522def _assertTest(): 523 """Run a test by comparing results to those expected and only failing if something is wrong. 524 525 Use _printTest to generate data for this test. 526 """ 527 # format is a set of lists of: 528 # - dms string 529 # - comment 530 # - should work (True or False) 531 # - splitStr: the expected output from splitDMSStr 532 # - degVal: the expected output from degFromDMSStr 533 # - secVal: the expected output from secFromDMSStr 534 # - neatStr: the expected output from neatenDMSStr 535 # - a list of three expected outputs from dmsStrFromDeg with nFields=3 and: 536 # - precision = 0 537 # - precision = 1 538 # - precision = 2 539 testSet = ( 540 ['::', '', True, ['' , '', '', '', ''], 0.0, 0.0, '0:00:00', ['0:00:00', '0:00:00.0', '0:00:00.00']], 541 ['-::', '', True, ['-', '', '', '', ''], -0.0, -0.0, '0:00:00', ['0:00:00', '0:00:00.0', '0:00:00.00']], 542 ['-0:00:00.01', '', True, ['-', '0', '00', '00', '.01'], -2.7777777777777775e-06, -0.01, '-0:00:00.01', ['-0:00:00', '-0:00:00.0', '-0:00:00.01']], 543 [' +1', '', True, ['+', '1', ''], 1.0, 1.0, '1', ['1:00:00', '1:00:00.0', '1:00:00.00']], 544 ['-1.2345', '', True, ['-', '1', '.2345'], -1.2344999999999999, -1.2344999999999999, '-1.2345', ['-1:14:04', '-1:14:04.2', '-1:14:04.20']], 545 ['-123::', '', True, ['-', '123', '', '', ''], -123.0, -442800.0, '-123:00:00', ['-123:00:00', '-123:00:00.0', '-123:00:00.00']], 546 ['-123:4', 'make sure seconds field is not 60 from dmsStrFromDeg', True, ['-', '123', '4', ''], -123.06666666666666, -7384.0, '-123:04', ['-123:04:00', '-123:04:00.0', '-123:04:00.00']], 547 ['-123:45', '', True, ['-', '123', '45', ''], -123.75, -7425.0, '-123:45', ['-123:45:00', '-123:45:00.0', '-123:45:00.00']], 548 ['-123:4.56789', '', True, ['-', '123', '4', '.56789'], -123.0761315, -7384.5678900000003, '-123:04.56789', ['-123:04:34', '-123:04:34.1', '-123:04:34.07']], 549 ['-123:45.6789', '', True, ['-', '123', '45', '.6789'], -123.761315, -7425.6788999999999, '-123:45.6789', ['-123:45:41', '-123:45:40.7', '-123:45:40.73']], 550 ['1:2:', '', True, ['', '1', '2', '', ''], 1.0333333333333334, 3720.0, '1:02:00', ['1:02:00', '1:02:00.0', '1:02:00.00']], 551 ['1:2:3', '', True, ['', '1', '2', '3', ''], 1.0341666666666667, 3723.0, '1:02:03', ['1:02:03', '1:02:03.0', '1:02:03.00']], 552 ['1:2:3.456789', '', True, ['', '1', '2', '3', '.456789'], 1.0342935525000001, 3723.4567889999998, '1:02:03.456789', ['1:02:03', '1:02:03.5', '1:02:03.46']], 553 ['1:23:4', '', True, ['', '1', '23', '4', ''], 1.3844444444444444, 4984.0, '1:23:04', ['1:23:04', '1:23:04.0', '1:23:04.00']], 554 ['1:23:45', '', True, ['', '1', '23', '45', ''], 1.3958333333333333, 5025.0, '1:23:45', ['1:23:45', '1:23:45.0', '1:23:45.00']], 555 ['123:45:6.789', '', True, ['', '123', '45', '6', '.789'], 123.75188583333333, 445506.78899999999, '123:45:06.789', ['123:45:07', '123:45:06.8', '123:45:06.79']], 556 ['123:45:56.789', '', True, ['', '123', '45', '56', '.789'], 123.76577472222222, 445556.78899999999, '123:45:56.789', ['123:45:57', '123:45:56.8', '123:45:56.79']], 557 ['-0::12.34', 'bug test; the sign must be retained', True, ['-', '0', '', '12', '.34'], -0.0034277777777777779, -12.34, '-0:00:12.34', ['-0:00:12', '-0:00:12.3', '-0:00:12.34']], 558 ['-::12.34', 'a weird gray area, but it works', True, ['-', '', '', '12', '.34'], -0.0034277777777777779, -12.34, '-0:00:12.34', ['-0:00:12', '-0:00:12.3', '-0:00:12.34']], 559 ['::12.34', '', True, ['', '', '', '12', '.34'], 0.0034277777777777779, 12.34, '0:00:12.34', ['0:00:12', '0:00:12.3', '0:00:12.34']], 560 ['1:23.4567', '', True, ['', '1', '23', '.4567'], 1.3909450000000001, 83.456699999999998, '1:23.4567', ['1:23:27', '1:23:27.4', '1:23:27.40']], 561 ['-1.234567', '', True, ['-', '1', '.234567'], -1.234567, -1.234567, '-1.234567', ['-1:14:04', '-1:14:04.4', '-1:14:04.44']], 562 ['-1:abadstr', 'invalid characters', False, None, None, None, None, None], 563 ['-1:2343:24', 'too many minutes digits', False, None, None, None, None, None], 564 ['1:-1:24', 'minus sign in wrong place', False, None, None, None, None, None], 565 ) 566 def locAssert(fmt, res, func, *args, **kargs): 567 assert fmt % (res,) == fmt % (func(*args, **kargs),), "%r != %r = %s(*%r, **%r)" % (res, func(*args, **kargs), func.__name__, args, kargs) 568 569 nErrors = 0 570 for testStr, commentStr, isOK, splitStr, degVal, secVal, neatStr, dmsStr02 in testSet: 571 try: 572 locAssert("%r", splitStr, splitDMSStr, testStr) 573 locAssert("%.8g", degVal, degFromDMSStr, testStr) 574 locAssert("%.8g", secVal, secFromDMSStr, testStr) 575 locAssert("%r", neatStr, neatenDMSStr, testStr) 576 locAssert("%r", dmsStr02[0], dmsStrFromDeg, degVal, 3, 0) 577 locAssert("%r", dmsStr02[1], dmsStrFromDeg, degVal, 3, 1) 578 locAssert("%r", dmsStr02[2], dmsStrFromDeg, degVal, 3, 2) 579 if not isOK: 580 print("unexpected success on %r" % testStr) 581 nErrors += 1 582 except Exception as e: 583 if isOK: 584 raise 585 print("unexpected failure on %r\n\t%s\nskipping other tests on this value" % (testStr, e)) 586 nErrors += 1 587 588 if nErrors == 0: 589 print("RO.StringUtil passed") 590 else: 591 print("RO.StringUtil failed with %d errors" % nErrors) 592 593 594def _printTest(dmsSet = None): 595 """Print the results of running each routine on a set of test data. 596 Data format is a list of tuples, each containing two elements: 597 dmsStr to test, a comment 598 599 The output is in the format used by _assertTest, but please use this with great caution. 600 You must examine the output very carefully to confirm it is correct before updating _assertTest! 601 """ 602 print("Exercising RO string utilities") 603 if not dmsSet: 604 dmsSet = ( 605 ("::", ""), 606 ("-::", ""), 607 ('-0:00:00.01', ""), 608 (" +1", ""), 609 ('-1.2345', ''), 610 ('-123::', ''), 611 ('-123:4', 'make sure seconds field is not 60 from dmsStrFromDeg'), 612 ('-123:45', ''), 613 ('-123:4.56789', ''), 614 ('-123:45.6789', ''), 615 ('1:2:', ''), 616 ('1:2:3', ''), 617 ('1:2:3.456789', ''), 618 ('1:23:4', ''), 619 ('1:23:45', ''), 620 ('123:45:6.789', ''), 621 ('123:45:56.789', ''), 622 ('-0::12.34', 'bug test; the sign must be retained'), 623 ('-::12.34', 'a weird gray area, but it works'), 624 ('::12.34', ''), 625 ('1:23.4567', ''), 626 ('-1.234567', ''), 627 ('-1:abadstr', 'invalid characters'), 628 ('-1:2343:24', 'too many minutes digits'), 629 ('1:-1:24', 'minus sign in wrong place'), 630 ) 631 632 for testStr, commentStr in dmsSet: 633 # note: if splitDMSStr succeeds, then the other calls all should succeed 634 if checkDMSStr(testStr): 635 try: 636 itemList = splitDMSStr(testStr) 637 deg = degFromDMSStr (testStr) 638 sec = secFromDMSStr (testStr) 639 neatStr = neatenDMSStr (testStr) 640 outDMSStr = [] 641 for prec in range(3): 642 outDMSStr.append(dmsStrFromDeg(deg, precision=prec)) 643 print("[%r, %r, True, %r, %r, %r, %r, %r]," % (testStr, commentStr, itemList, deg, sec, neatStr, outDMSStr)) 644 except Exception as e: 645 print("unexpected failure on %r (%s); error = %s" % (testStr, commentStr, e)) 646 else: 647 print("[%r, %r, False, %r, %r, %r, %r, %r]," % tuple([testStr, commentStr] + [None]*5)) 648 649if __name__ == "__main__": 650 doPrint = False 651 if doPrint: 652 _printTest() 653 else: 654 _assertTest() 655