# -*- coding: utf-8 -*- """ novelWriter – Common Functions ============================== Various common functions File History: Created: 2019-05-12 [0.1.0] This file is a part of novelWriter Copyright 2018–2021, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging from datetime import datetime from PyQt5.QtWidgets import qApp from PyQt5.QtCore import QCoreApplication from nw.constants import ( nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout ) logger = logging.getLogger(__name__) def checkString(value, default, allowNone=False): """Check if a variable is a string or a none. """ if allowNone: if value is None: return None if value == "None": return None if isinstance(value, str): return str(value) return default def checkInt(value, default, allowNone=False): """Check if a variable is an integer or a none. """ if allowNone: if value is None: return None if value == "None": return None try: return int(value) except Exception: return default def checkBool(value, default, allowNone=False): """Check if a variable is a boolean or a none. """ if allowNone: if value is None: return None if value == "None": return None if isinstance(value, str): if value == "True": return True elif value == "False": return False else: return default elif isinstance(value, int): if value == 1: return True elif value == 0: return False else: return default return default def checkHandle(value, default, allowNone=False): """Check if a value is a handle. """ if allowNone: if value is None: return None if value == "None": return None if isHandle(value): return str(value) return default def isHandle(theString): """Check if a string is a valid novelWriter handle. Note: This is case sensitive. Must be lower case! """ if not isinstance(theString, str): return False if len(theString) != 13: return False for c in theString: if c not in "0123456789abcdef": return False return True def isTitleTag(theString): """Check if a string is a valid title string. """ if not isinstance(theString, str): return False if len(theString) != 7: return False if not theString.startswith("T"): return False for c in theString[1:]: if c not in "0123456789": return False return True def isItemClass(theString): """Check if an item is a calid nwItemClass identifier. """ return theString in nwItemClass.__members__ def isItemType(theString): """Check if an item is a calid nwItemType identifier. """ return theString in nwItemType.__members__ def isItemLayout(theString): """Check if an item is a calid nwItemLayout identifier. """ return theString in nwItemLayout.__members__ def hexToInt(value, default=0): """Convert a hex string to an integer. """ if isinstance(value, str): try: return int(value, 16) except Exception: return default return default def formatInt(theInt): """Formats an integer with k, M, G etc. """ postFix = ["k", "M", "G", "T", "P", "E"] theVal = float(theInt) if theVal > 1000.0: for pF in postFix: theVal /= 1000.0 if theVal < 1000.0: if theVal < 10.0: return f"{theVal:4.2f}{nwUnicode.U_THSP}{pF}" elif theVal < 100.0: return f"{theVal:4.1f}{nwUnicode.U_THSP}{pF}" else: return f"{theVal:3.0f}{nwUnicode.U_THSP}{pF}" return str(theInt) def formatTimeStamp(theTime, fileSafe=False): """Take a number (on the format returned by time.time()) and convert it to a timestamp string. """ if fileSafe: return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP) else: return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP) def formatTime(tS): """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format if a full day or longer. """ if isinstance(tS, int): if tS >= 86400: return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" else: return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" return "ERROR" def splitVersionNumber(vString): """ Splits a version string on the form aa.bb.cc into major, minor and patch, and computes an integer value aabbcc. """ vMajor = 0 vMinor = 0 vPatch = 0 vInt = 0 vBits = vString.split(".") nBits = len(vBits) if nBits > 0: vMajor = checkInt(vBits[0], 0) if nBits > 1: vMinor = checkInt(vBits[1], 0) if nBits > 2: vPatch = checkInt(vBits[2], 0) vInt = vMajor*10000 + vMinor*100 + vPatch return [vMajor, vMinor, vPatch, vInt] def transferCase(theSource, theTarget): """Transfers the case of the source word to the target word. This will consider all upper or lower, and first char capitalisation. """ theResult = theTarget if not isinstance(theSource, str) or not isinstance(theTarget, str): return theResult if len(theTarget) < 1 or len(theSource) < 1: return theResult if theSource.istitle(): theResult = theTarget.title() if theSource.isupper(): theResult = theTarget.upper() elif theSource.islower(): theResult = theTarget.lower() return theResult def fuzzyTime(secDiff): """Converts a time difference in seconds into a fuzzy time string. """ if secDiff < 0: return QCoreApplication.translate( "Common", "in the future" ) elif secDiff < 30: return QCoreApplication.translate( "Common", "just now" ) elif secDiff < 90: return QCoreApplication.translate( "Common", "a minute ago" ) elif secDiff < 3300: # 55 minutes return QCoreApplication.translate( "Common", "{0} minutes ago" ).format(int(round(secDiff/60))) elif secDiff < 5400: # 90 minutes return QCoreApplication.translate( "Common", "an hour ago" ) elif secDiff < 84600: # 23.5 hours return QCoreApplication.translate( "Common", "{0} hours ago" ).format(int(round(secDiff/3600))) elif secDiff < 129600: # 1.5 days return QCoreApplication.translate( "Common", "a day ago" ) elif secDiff < 561600: # 6.5 days return QCoreApplication.translate( "Common", "{0} days ago" ).format(int(round(secDiff/86400))) elif secDiff < 907200: # 10.5 days return QCoreApplication.translate( "Common", "a week ago" ) elif secDiff < 2419200: # 28 days return QCoreApplication.translate( "Common", "{0} weeks ago" ).format(int(round(secDiff/604800))) elif secDiff < 3888000: # 45 days return QCoreApplication.translate( "Common", "a month ago" ) elif secDiff < 29808000: # 345 days return QCoreApplication.translate( "Common", "{0} months ago" ).format(int(round(secDiff/2592000))) elif secDiff < 47336400: # 1.5 years return QCoreApplication.translate( "Common", "a year ago" ) else: return QCoreApplication.translate( "Common", "{0} years ago" ).format(int(round(secDiff/31557600))) def makeFileNameSafe(theText): """Returns a filename safe version of the text. """ cleanName = "" for c in theText.strip(): if c.isalpha() or c.isdigit() or c == " ": cleanName += c return cleanName def getGuiItem(theName): """Returns a QtWidget based on its objectName. """ for qWidget in qApp.topLevelWidgets(): if qWidget.objectName() == theName: return qWidget return None def numberToRoman(numVal, isLower=False): """Convert an integer to a roman number. """ if not isinstance(numVal, int): return "NAN" if numVal < 1 or numVal > 4999: return "OOR" theValues = [ (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"), ] romNum = "" for theDiv, theSym in theValues: n = numVal//theDiv romNum += n*theSym numVal -= n*theDiv if numVal <= 0: break return romNum.lower() if isLower else romNum