├── .gitignore ├── LICENSE_MIT.md ├── README.md ├── bin └── pubdocs ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── settings.gradle.kts └── src ├── commonMain └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ ├── LogFormatter.kt │ └── Loggable.kt │ ├── mp_tools │ ├── AsyncBouncer.kt │ ├── ByteArrayTools.kt │ ├── CachedExpression.kt │ ├── CachedRefreshingValue.kt │ ├── async_tools.kt │ ├── base64.kt │ ├── reentrant_mutex.kt │ ├── text_tools.kt │ └── time_tools_experimental.kt │ └── sprintf │ ├── ExponentFormatter.kt │ ├── Specification.kt │ └── Sprintf.kt ├── commonTest └── kotlin │ ├── sprintf │ └── SprintfTest.kt │ └── tools.kt ├── iosArm64Main └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── ConsoleLoggerSetup.kt │ └── sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt ├── iosSimulatorArm64Main └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── ConsoleLoggerSetup.kt │ └── sprintf │ ├── Specification.iosSimulatorArm64.kt │ └── Sprintf.iosSimulatorArm64.kt ├── iosX64Main └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── ConsoleLoggerSetup.kt │ └── sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt ├── jsMain └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── ConsoleLoggerSetup.kt │ └── sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt ├── jvmMain └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ ├── ConsoleLoggerSetup.kt │ └── FileLogCatcher.kt │ ├── sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt │ └── tools │ └── CachedSyncExpression.kt ├── jvmTest └── kotlin │ ├── mo_logger │ ├── TestLogger.kt │ └── testtoolsJVM.kt │ └── net │ └── sergeych │ └── jvm_tools.kt ├── macosArm64Main └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── Loggable.macosArm64.kt │ └── sprintf │ ├── Specification.macosArm64.kt │ └── Sprintf.macosArm64.kt ├── macosX64Main └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ └── Loggable.macosX64.kt │ └── sprintf │ ├── Specification.macosX64.kt │ └── Sprintf.macosX64.kt ├── mingwX64Main └── kotlin │ ├── net.sergeych.sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt │ └── net │ └── sergeych │ └── mp_logger │ └── Loggable.mingwX64.kt ├── nativeMain └── kotlin │ └── net │ └── sergeych │ ├── mp_logger │ ├── ConsoleLoggerSetup.kt │ └── toConsole.kt │ └── sprintf │ ├── ConvertToInstant.kt │ └── LocaleSpecificMonthName.kt └── wasmJsMain └── kotlin └── net └── sergeych ├── mp_logger └── Loggable.wasmJs.kt └── sprintf ├── Specification.wasmJs.kt └── Sprintf.wasmJs.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | .idea 4 | /build/ 5 | /build/classes/kotlin/jvm/main/ 6 | /build/classes/kotlin/jvm/test/ 7 | /build/classes/kotlin/native/main/klib/ 8 | /testlog* 9 | .kotlin 10 | /.gigaide/gigaide.properties 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /LICENSE_MIT.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Sergey S. Chernov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KMP Sergeych's tools 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | > Important: Versions 1.5.1+ is built with kotlin 2 and is compatible with ALL KMP platforms including experimentlas 6 | > wasmJS. It also contains important fix on displaying negative integers in some formats. 7 | 8 | ## 1.5.2 is built for all target, ios and wasmjs included! 9 | 10 | > See also [dokka docs](https://code.sergeych.net/docs/mp_stools/) 11 | 12 | Kotlin Multiplatform important missing tools, like sprintf with a wide variety of formats, portable base64 13 | 14 | # Why reinventing the wheel? 15 | 16 | When I started to write our applications and libraries in KMP mode, my code worked the same on all of the plaforms we 17 | develop for. Many tools our team is used to do not exist on all platforms or exist with different 18 | interfaces. So, I've started to write portable interfaces to it that work everywhere and _with the same interface_ on 19 | all three platforms. 20 | 21 | ## In short, this library provides: 22 | 23 | All platforms (macosX64, macosArm64, iosX64, iosArm64, iosSimulatorArm64, 24 | linuxX6, mingwX64, JVM, JS, wasmJS), in the same way: 25 | 26 | - `Stirng.sprintf` - something like C `sprinf` or JVM `String.format` but with more features and multiplatform 27 | 28 | - base64: `ByteArray.encodeToBase64()`, `ByteArray.encodeToBase64Compact()`, `String.decodeBase64()` 29 | and `ByteArray.decodeBase64Compact()`. Also, URL-friendly forms: `ByteArray.encodeToBase64Url` 30 | and `String.decodeBase64Url`. 31 | 32 | - Boyer-Moore based fast `ByteArray.indexOf` 33 | 34 | - ByteArray tools: `getInt`, `putInt` and fast `indexOf` 35 | - Tools to cache recalculable expressions: `CachedRefreshingValue`, `CachedExpression` and `CachedSyncExpression` for 36 | JVM (as a good multithreading is there) 37 | - Missing `ReenterantMutex` for coroutines 38 | - Smart, fast and effective _asynchronous logging_, coroutine-based, using flows ti subscribe to logs and coroutines and 39 | closures not to waste time on preparing strings where logging level filters are out anyway. 40 | 41 | ## Installation 42 | 43 | Use gradle maven dependency. First add our repository: 44 | 45 | ~~~ 46 | repositories { 47 | // ... 48 | maven("https://maven.universablockchain.com/") 49 | } 50 | ~~~ 51 | 52 | then add dependency: 53 | 54 | ~~~ 55 | dependencies { 56 | //... 57 | // see versions explained below, use latest release from 58 | // 'releases' or whatever you need: 59 | implementation("net.sergeych:mp_stools:1.5.2") 60 | } 61 | ~~~ 62 | 63 | That's all. Now you have working `sprintf` on every KMP platform ;) 64 | 65 | # String tools: 66 | 67 | ## printf / sprintf! 68 | 69 | The most popular and known stromg format tool exists only on the modern JVM platforms, 70 | so I reimplement it in a platform-independent way. 71 | Here are some examples, the reference is below it. 72 | I reporoduced 73 | the [Java 11 String.format() notation](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter) 74 | as much as possible, with the following notable differences: 75 | 76 | - for argument number (`%1$12s`) is possible also to use `!` instead of `$` (as the latter should be escaped in kotlin), 77 | e.g. `%1!12s` in that case is _also valid_ 78 | - date/time per-platform locales are not yet supported, everything is in English 79 | - time zone abbreviations are missing (system returns valid tz id like +01:00 instead), as kotlinx.time does not 80 | provide (yet?) 81 | 82 | see reference below 83 | 84 | ### Integers 85 | 86 | ~~~kotlin 87 | // Integers 88 | assertEquals("== 3 ==", "== %d ==".sprintf(3)) 89 | assertEquals("== 3 ==", "== %3d ==".sprintf(3)) 90 | assertEquals("== 3 ==", "== %-3d ==".sprintf(3)) 91 | assertEquals("== 3 ==", "== %^3d ==".sprintf(3)) 92 | 93 | // fill 94 | assertEquals("== 003 ==", "== %03d ==".sprintf(3)) 95 | assertEquals("== **3** ==", "== %*^5d ==".sprintf(3)) 96 | assertEquals("== __3__ ==", "== %_^5d ==".sprintf(3)) 97 | 98 | // leading plus 99 | assertEquals("== +3 ==", "== %+d ==".sprintf(3)) 100 | assertEquals("== +0003 ==", "== %+05d ==".sprintf(3)) 101 | 102 | // hex 103 | assertEquals("== 1e ==", "== %x ==".sprintf(0x1e)) 104 | assertEquals("== 1E ==", "== %X ==".sprintf(0x1e)) 105 | 106 | // filled hex 107 | assertEquals("== ###1e ==", "== %#5x ==".sprintf(0x1e)) 108 | assertEquals("== 1e### ==", "== %#-5x ==".sprintf(0x1e)) 109 | assertEquals("== ##1E## ==", "== %#^6X ==".sprintf(0x1e)) 110 | 111 | // escaping percent 112 | assertEquals("10%", "10%%".sprintf()) 113 | ~~~ 114 | 115 | ### Strings (or anything as string) 116 | 117 | Texts works with any object, using it's `toString()`, also with numbers, wit the same positioning, size and fill flags: 118 | 119 | ~~~kotlin 120 | // regular strings 121 | assertEquals("*****hello!", "%*10s!".sprintf("hello")) 122 | assertEquals("Hello, world!", "%s, %s!".sprintf("Hello", "world")) 123 | assertEquals("___centered___", "%^_14s".sprintf("centered")) 124 | 125 | // number as anything else are processed using `toString()`: 126 | assertEquals("== 3 ==", "== %s ==".sprintf(3)) 127 | assertEquals("== 3 ==", "== %3s ==".sprintf(3)) 128 | assertEquals("== 3 ==", "== %-3s ==".sprintf(3)) 129 | assertEquals("== 3 ==", "== %^3s ==".sprintf(3)) 130 | assertEquals("== **3** ==", "== %*^5s ==".sprintf(3)) 131 | assertEquals("== __3__ ==", "== %_^5s ==".sprintf(3)) 132 | ~~~ 133 | 134 | ### Floats 135 | 136 | Any `Number` instances (we tried integer, long, float and double) can be formatted with `%g`, `%f` and `%e` specifiers: 137 | 138 | ~~~kotlin 139 | // best fit, platofrm-dependent "good" representation: 140 | assertEquals("17.234", "%g".sprintf(17.234)) 141 | assertEquals("**17.234", "%*8g".sprintf(17.234)) 142 | assertEquals("+017.234", "%+08g".sprintf(17.234)) 143 | 144 | // Scientific format: 145 | assertEquals("-2.39E-3", "%.2E".sprintf(-2.39e-3)) 146 | assertEquals("2.39E-3", "%.2E".sprintf(2.39e-3)) 147 | assertEquals("+2.39E-3", "%+.2E".sprintf(2.39e-3)) 148 | 149 | assertEquals("2.4E-3", "%6E".sprintf(2.39e-3)) 150 | assertEquals("0002.4E-3", "%09.1E".sprintf(2.39e-3)) 151 | assertEquals("+002.4E-3", "%+09.1E".sprintf(2.39e-3)) 152 | 153 | // format with decimal part of fixed with (no exponent): 154 | assertEquals("1.000", "%.3f".sprintf(1)) 155 | assertEquals("221.122", "%.3f".sprintf(221.1217)) 156 | assertEquals("__221.1", "%_7.1f".sprintf(221.1217)) 157 | assertEquals("_+221.1", "%+_7.1f".sprintf(221.1217)) 158 | assertEquals("+0221.1", "%+07.1f".sprintf(221.1217)) 159 | assertEquals("00221.1", "%07.1f".sprintf(221.1217)) 160 | ~~~ 161 | 162 | ### Safety notes 163 | 164 | This sprintf/format implementation is safe on all platforms 165 | as it has no dependencies except standard `Number.toString()`, 166 | which is presumably safe. 167 | Despite its name, it does not call `C` library, 168 | uses controlled memory allocation and could not provide overruns, as kotlin arrays are all checked. 169 | 170 | ### Sprintf syntax summary 171 | 172 | Generic format field has the following notation: 173 | 174 | %[flags][size][.decimals] 175 | 176 | flags and size are optional, there could be several flags. The size field is used to pad the result to specified size, 177 | padding is added with spaces before the value by default; this behavior could be changed with flags, see 178 | below. `decimals` where applicable takes precedence over size, and determines how many decimal digits will be included, 179 | e.g. `"%.3f".sprintf(1) == "1.000"` 180 | 181 | If the argument is wider than the `size`, it is inserted as it is ignoring positioning flags and `size` field. 182 | 183 | #### flags 184 | 185 | | flag | sample | meaning | applicable | 186 | |-------------|---------|-------------------------------------------------|-------------------------| 187 | | `-` | `%-5d` | adjust to left | with size | 188 | | `^` | `%12s` | center | with size | 189 | | `*` `#` `_` | `%*10s` | fill with specified character | with size | 190 | | `0` | `%010d` | fill with leading zeroes | with size, only numbers | 191 | | `+` | `%+d` | explicitly show `+` sign with _positive numbers | with numbers only | 192 | 193 | #### Supported format specificators 194 | 195 | As for now: 196 | 197 | | format | meaning | consumed argument type | 198 | |------------|--------------------------------------------------------------------|-------------------------| 199 | | `s` | text representation (string or anything else) | `Any` | 200 | | `c` | signle character | `Char` | 201 | | `d` or `i` | as integer number | any `Number` | 202 | | `x` | hexadecimal number, lowercase characters | any integer type | 203 | | `X` | hexadecimal number, uppercase characters | any integer type | 204 | | `o` | octal number | any integer type | 205 | | `f` | float number, fixed decimal points, respects `decimals` field | any `Number` | 206 | | `g`, `G` | platorm-dependedn 'best fit' float number, ingnores `decimals`. | any `Number` | 207 | | `e`, `E` | float, scientific notation with exponent, respect `decimals` field | any `Number` | 208 | | `t*` | date time, see below | differnet time objects | 209 | | `%` | insert percent character | no argument is consumer | 210 | 211 | In `g`/`G` and `e`/`E` formats the case of the result exponent character is the same as for the format character. 212 | 213 | ## Date/time formatting 214 | 215 | We support 216 | the [Java 11 String.format() notation](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter.html#syntax) 217 | as much as possible here too. 218 | 219 | To format a time object, it is possible to use: 220 | 221 | - multiplatform (recommended!) `kotlinx.datetime` classes: `Instant` and `LocalDateTime`. 222 | - on JS platoform also javascript `Date` class instances are also ok 223 | - on JVM platofm you can also use `java.time` classes: `java.time.Instant`, java.time.LocalDateTime` and ` 224 | java.time.ZonedDateTime` as well. Zoned date time will be converted to the system's default time zone (e.g., its time 225 | zone 226 | information will be lost). 227 | 228 | Supported are all standard format specifiers. 229 | 230 | #### Time formats 231 | 232 | | format | meaning | 233 | |--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 234 | | `tH` | Hour of the day for the 24-hour clock, formatted as two digits with a leading zero as necessary i.e. 00 - 23. | 235 | | `tI` | Hour for the 12-hour clock, formatted as two digits with a leading zero as necessary, i.e. 01 - 12. | 236 | | `tk` | Hour of the day for the 24-hour clock, i.e. 0 - 23. | 237 | | `tl` | Hour for the 12-hour clock, i.e. 1 - 12. | 238 | | `tM` | Minute within the hour formatted as two digits with a leading zero as necessary, i.e. 00 - 59. | 239 | | `tS` | Seconds within the minute, formatted as two digits with a leading zero as necessary, i.e. 00 - 59 | 240 | | `tL` | Millisecond within the second formatted as three digits with leading zeros as necessary, i.e. 000 - 999. | 241 | | `tN` | Nanosecond within the second, formatted as nine digits with leading zeros as necessary, i.e. 000000000 - 999999999. | 242 | | `tP` | Locale-specific morning or afternoon marker in lower case, e.g."am" or "pm". Use of the conversion prefix 'T' forces this output to upper case. | 243 | | `tz` | RFC 822 style numeric time zone offset from GMT, e.g. -0800. This value will be adjusted as necessary for Daylight Saving Time. For long, Long, and Date the time zone used is the default time zone for this instance of the Java virtual machine. | 244 | | `tZ` | A string representing the abbreviation for the time zone. Not fully supported | 245 | | `ts` | Seconds since the beginning of the epoch starting at 1 January 1970 00:00:00 UTC, i.e. Long.MIN_VALUE/1000 to Long.MAX_VALUE/1000. | 246 | | `tQ` | Milliseconds since the beginning of the epoch starting at 1 January 1970 00:00:00 UTC, i.e. Long.MIN_VALUE to Long.MAX_VALUE. | 247 | 248 | #### Date formats 249 | 250 | Note. If the locale is not implemented for the platform, English names are used automatically. 251 | 252 | | format | meaning | 253 | |--------|-----------------------------------------------------------------------------------------------------------------------------| 254 | | `tB` | Locale-specific full month name, e.g. "January", "February". | 255 | | `tb` | Locale-specific abbreviated month name, e.g. "Jan", "Feb". | 256 | | `th` | same as `tb` | 257 | | `tA` | Locale-specific full name of the day of the week, e.g. "Sunday", "Monday" | 258 | | `ta` | Locale-specific short name of the day of the week, e.g. "Sun", "Mon" | 259 | | `tC` | __Not implemented. Please use `ty`__ | 260 | | `tY` | Year, formatted as at least four digits with leading zeros as necessary, e.g. 0092 equals 92 CE for the Gregorian calendar. | 261 | | `ty` | Last two digits of the year, formatted with leading zeros as necessary, i.e. 00 - 99. | 262 | | `tj` | Day of year, formatted as three digits with leading zeros as necessary, e.g. 001 - 366 for the Gregorian calendar. | 263 | | `tm` | Month, formatted as two digits with leading zeros as necessary, i.e. 01 - 13. | 264 | | `td` | Day of month, formatted as two digits with leading zeros as necessary, i.e. 01 - 31 | 265 | | `te` | Day of month, formatted as two digits, i.e. 1 - 31. | 266 | 267 | #### Date+time compositions 268 | 269 | | format | meaning | 270 | |--------|---------------------------------------------------------------------------------------------------------------------------------------------| 271 | | `tR` | Time formatted for the 24-hour clock as "%tH:%tM" | 272 | | `tT` | Time formatted for the 24-hour clock as "%tH:%tM:%tS". | 273 | | `tr` | Time formatted for the 12-hour clock as "%tI:%tM:%tS %Tp". The location of the morning or afternoon marker ('%Tp') may be locale-dependent. | 274 | | `tD` | Date formatted as "%tm/%td/%ty". | 275 | | `tF` | ISO 8601 complete date formatted as "%tY-%tm-%td" | 276 | | `tc` | Date and time formatted as "%ta %tb %td %tT %tZ %tY", e.g. "Thu May 06 05:45:11 +01:00 1970". | 277 | 278 | #### Extensions 279 | 280 | | format | meaning | 281 | |--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 282 | | `tO` | Popular ISO8601 variant, like 1970-06-05T05:41:11+03:00. Not a digit, letter `O` | 283 | | `t#` | 140letter "serial time" in UTC zone, like `20220414132756` year, month, day, hour, minute and second all together with leading zeroes ib 24h mode, for example, to use in file names | 284 | 285 | ### Notes 286 | 287 | _note that there is two variants `"%s".sprintf()` and `"%s".format` but the latter is already used in JVM and may 288 | confuse._ 289 | 290 | ## Base64 291 | 292 | Why? 293 | Because, for example, in JS, 294 | there is no good way to convert to/from ByteArray 295 | (or Uint8Array)that always works well and does not require NPM dependencies that work synchronously. 296 | 297 | I know how it could be made almost portable with promises, though. So, here is an implementation that works well 298 | everywhere with the same interface. 299 | The wheel is reinvented one more time. 300 | 301 | ~~~ 302 | val src = byteArrayOf(1,3,4,4) 303 | assertEquals(src.encodeToBase64Compact(), "AQMEBA") 304 | assertEquals(src.encodeToBase64(), "AQMEBA==") 305 | assertContentEquals(src, "AQMEBA".decodeBase64Compact()) 306 | assertContentEquals(src, "AQMEBA==".decodeBase64()) 307 | ~~~ 308 | 309 | __Compact__ vartiant simply does not use trailing filling '=' characters, these are practically useless but taking 310 | space. 311 | 312 | ## Minimal logger 313 | 314 | _logging is not working in the kotlin.native platform as it is yet single-threaded in the core and does not support 315 | shared objects sucj as flow (as for now)_. 316 | 317 | Library provides an extremely compact and effective platform-independent asyncronous logger that uses coroutines to provide 318 | little performance impact. The idea behind is that the logging data is collected and formatted _conditionally_: instead 319 | of providing strings with substitutions we provide callables that returns strings or string to exception pairs: 320 | 321 | ~~~kotlin 322 | debug { "this is a trace: ${Math.sin(Math.PI)}" } 323 | ~~~ 324 | 325 | The string is rather slow in interpolation as it uses `Math.sin`. But, (1) it will not be interpolated if effective log 326 | level is above the `Log.Level.Debug`, and (2) if it is, it will be interpolated asyncronously, maybe in a separate 327 | thread or when this thread become idle. A coroutine context is used to prepare the data to be logged. 328 | 329 | To start logging, implement an [Loggable] interface in your class, and connect some log sinks: 330 | 331 | ~~~kotlin 332 | val x = object : Loggable by LogTag("TSTOB") {} 333 | x.info { "that should not be missing because of the replay buffer" } 334 | Log.connectConsole() 335 | ~~~ 336 | 337 | To receive log messages (asynchronously) use `Log.logFlow` shared flow, or connect some stabdard receiver like console 338 | one as in the sample above. 339 | 340 | # Future 341 | 342 | The library is actively maintained and is going to be maintained for a long time; it is already used in many commercial projects). 343 | 344 | # Mac-compatible releases 345 | 346 | I release Apple compatible releases when I have access to Macs. Normally I do not use Apple or Microsoft OS, sticking with Linux; 347 | I have serious concerns about privacy and safety on these. Everybody now see what happened to all poor apple customers 348 | born in improper country as for their lets see far too mature president ;) Therefore I stick to linux, and traveling with two notebooks is against customs regulations everywhere, so non-snapshot full releases are not too frequent. 349 | 350 | Meanwhile I encourage using snapshot releases except for Apple targets, these are frequent and I increase minor versions 351 | on every notable change, so it should be safe. Thanks for understanding -- wnd I recommend considering wide use of free open source platforms. 352 | -------------------------------------------------------------------------------- /bin/pubdocs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | ./gradlew dokkaHtml 4 | rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/mp_stools 5 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | //@file:Suppress("UNUSED_VARIABLE") 2 | 3 | plugins { 4 | kotlin("multiplatform") version "2.0.20" 5 | kotlin("plugin.serialization") version "2.0.20" 6 | id("org.jetbrains.dokka") version "1.9.20" 7 | `maven-publish` 8 | } 9 | 10 | group = "net.sergeych" 11 | version = "1.5.2" 12 | 13 | val serialization_version = "1.6.3" 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | kotlin { 20 | jvm { 21 | compilations.all { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | withJava() 25 | testRuns["test"].executionTask.configure { 26 | useJUnitPlatform() 27 | } 28 | } 29 | linuxX64() 30 | linuxArm64() 31 | mingwX64() 32 | js(IR) { 33 | browser { 34 | commonWebpackConfig { 35 | // cssSupport.enabled = true 36 | } 37 | } 38 | } 39 | 40 | wasmJs { 41 | browser() 42 | binaries.executable() 43 | } 44 | 45 | // val hostOs = System.getProperty("os.name") 46 | // val isMingwX64 = hostOs.startsWith("Windows") 47 | // val nativeTarget = when { 48 | // hostOs == "Mac OS X" -> macosX64("native") 49 | // hostOs == "Linux" -> linuxX64("native") 50 | // isMingwX64 -> mingwX64("native") 51 | // else -> throw GradleException("Host OS is not supported in Kotlin/Native.") 52 | // } 53 | // 54 | // val publicationsFromMainHost = 55 | // listOf(jvm(), js()).map { it.name } + "kotlinMultiplatform" 56 | // linuxX64("native") { 57 | // binaries.staticLib { 58 | // baseName = "mp_bintools" 59 | // } 60 | // } 61 | 62 | listOf( 63 | iosX64(), 64 | iosArm64(), 65 | iosSimulatorArm64() 66 | ).forEach { 67 | it.binaries.framework { 68 | baseName = "mp_bintools" 69 | isStatic = true 70 | } 71 | } 72 | 73 | listOf( 74 | macosX64(), 75 | macosArm64() 76 | ).forEach { 77 | it.binaries.framework { 78 | baseName = "mp_bintools" 79 | isStatic = true 80 | } 81 | } 82 | 83 | sourceSets { 84 | all { 85 | // languageSettings.optIn("kotlin.RequiresOptIn") 86 | // languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") 87 | // languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 88 | languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") 89 | } 90 | val commonMain by getting { 91 | dependencies { 92 | api("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") 93 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") 94 | api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") 95 | } 96 | } 97 | val commonTest by getting { 98 | dependencies { 99 | implementation(kotlin("test")) 100 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") 101 | } 102 | } 103 | 104 | val nativeMain by creating { 105 | dependsOn(commonMain) 106 | dependencies { 107 | } 108 | } 109 | val linuxX64Main by getting { 110 | dependsOn(nativeMain) 111 | } 112 | val linuxArm64Main by getting { 113 | dependsOn(nativeMain) 114 | } 115 | // for (platform in listOf(linuxX64Main, mingwMain)) 116 | // platform { dependsOn(nativeMain) } 117 | 118 | val jvmMain by getting 119 | val jvmTest by getting 120 | val jsMain by getting 121 | val jsTest by getting 122 | val wasmJsMain by getting 123 | val wasmJsTest by getting 124 | } 125 | 126 | publishing { 127 | publications { 128 | 129 | // matching { it.name in publicationsFromMainHost }.all { 130 | // val targetPublication = this@all 131 | // tasks.withType() 132 | // .matching { it.publication == targetPublication } 133 | // .configureEach { onlyIf { findProperty("isMainHost") == "true" } } 134 | // } 135 | 136 | // create("maven") { 137 | // from(components["java"]) 138 | // } 139 | } 140 | repositories { 141 | maven { 142 | val mavenUser: String by project 143 | val mavenPassword: String by project 144 | url = uri("https://maven.universablockchain.com/") 145 | credentials { 146 | username = mavenUser 147 | password = mavenPassword 148 | } 149 | } 150 | 151 | } 152 | } 153 | } 154 | 155 | tasks.dokkaHtml.configure { 156 | outputDirectory.set(buildDir.resolve("dokka")) 157 | dokkaSourceSets { 158 | // configureEach { 159 | // includes.from("docs/bipack.md") 160 | // } 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | #kotlin.mpp.enableGranularSourceSetsMetadata=true 3 | #kotlin.native.enableDependencyPropagation=false 4 | kotlin.js.generate.executable.default=false 5 | kotlin.native.ignoreDisabledTargets=true 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergeych/mp_stools/724f4e005c571467c7d05a80f146ecd21b964178/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "mp_stools" 3 | 4 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_logger/LogFormatter.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.LocalDate 5 | import kotlinx.datetime.TimeZone 6 | import kotlinx.datetime.toLocalDateTime 7 | import net.sergeych.sprintf.sprintf 8 | 9 | /** 10 | * Format [LogEntry] to set of string (multiline output) implementing log start and data change markup. This 11 | * implementation is human-readable, plain text, in machine-parseable form 12 | */ 13 | open class LogFormatter { 14 | 15 | /** 16 | * The separator that begins and ends separate line inserted in the beginning of the log 17 | */ 18 | open val startDelimiter = "----------" 19 | 20 | /** 21 | * The separator that begins and ends separate line inserted when the date has been changed from the 22 | * last message. It is not inserter before the first entry. 23 | */ 24 | open val dateChangleDelimiter = "---" 25 | 26 | /** 27 | * Default timezone captured when the formatter is created. Note that subsequend default time zone changes will 28 | * not affect created formatter instance that may be in some cases not a desired implementation. Override it to 29 | * refresh default timezone if need. 30 | */ 31 | protected open val tz = TimeZone.currentSystemDefault() 32 | 33 | protected var lastDate: LocalDate? = null 34 | private set 35 | 36 | /** 37 | * Accumulates outout lines. Will be returned and cleared diring the next [format] call. 38 | */ 39 | protected val buffer by lazy { 40 | mutableListOf("%s log started: %tc %1!s".sprintf(startDelimiter, Clock.System.now())) 41 | } 42 | 43 | /** 44 | * Prepares a single entry to be shown. This version uses default formatting. 45 | */ 46 | open protected fun formatEntry(le: LogEntry) = le.toString() 47 | 48 | /** 49 | * format log entry to one or more strings to be emitted to the log console, file, etc. 50 | * @param le entry to be formatted 51 | * @return array (empty or with 1+ strings) to be displayed. 52 | */ 53 | open fun format(le: LogEntry): List { 54 | val now = Clock.System.now().toLocalDateTime(tz).date 55 | if (lastDate == null) lastDate = now 56 | else { 57 | if(now != lastDate) { 58 | buffer.add("%s Date changed: %s %1!s".sprintf(dateChangleDelimiter, now)) 59 | lastDate = now 60 | } 61 | } 62 | buffer.add(formatEntry(le)) 63 | return buffer.toList().also { buffer.clear() } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_logger/Loggable.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) 2 | @file:Suppress("VARIABLE_IN_SINGLETON_WITHOUT_THREAD_LOCAL") 3 | 4 | package net.sergeych.mp_logger 5 | 6 | import kotlinx.coroutines.* 7 | import kotlinx.coroutines.channels.BufferOverflow 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.asSharedFlow 10 | import kotlinx.datetime.Clock 11 | import kotlinx.datetime.Instant 12 | import kotlinx.serialization.SerialName 13 | import kotlinx.serialization.Serializable 14 | import net.sergeych.mp_logger.Log.connectConsole 15 | import net.sergeych.mp_logger.Log.logFlow 16 | import net.sergeych.sprintf.sprintf 17 | 18 | /** 19 | * Class/object instance that could be used to emit logging from. See [info], [debug], [warning], [error] and 20 | * [exception] Loggable excension functions. The interface is used as a workaround of missing multimple inheritance 21 | * in kolin, in most cases you just inherit from [LogTag]: 22 | * 23 | * ~~~kotlin 24 | * // simple case 25 | * class SimpleClass(): LogTag("SCLAS") { 26 | * fun test() { 27 | * debug { "test1" } 28 | * } 29 | * 30 | * // but if we need to inherit some other class, we need an interface: 31 | * 32 | * class StrangeException(text: String): Exception(text), Loggable by TagLog("SEXC") { 33 | * init { 34 | * debug { "StrangeException has been instantiated" } 35 | * } 36 | * } 37 | * ~~~ 38 | */ 39 | interface Loggable { 40 | /** 41 | * Tag attached to each emitted log entry. Better be 3-5 character long, though when really need it could 42 | * be any string. 43 | */ 44 | var logTag: String 45 | 46 | /** 47 | * Filtering level _for this instance only__. It takes precedence over [Log.defaultLevel] if not null, e.g 48 | * if the overall logging level is set ot [Log.Level.INFO] but this instance is set to [Log.Level.DEBUG], 49 | * the debug log records _from this instance_ will be accepted. 50 | */ 51 | var logLevel: Log.Level? 52 | } 53 | 54 | /** 55 | * A tag that allow to log with using one of [info], [debug], [warning], [error] and [exception]. 56 | * 57 | * It is open class, so you can just inherit from it. If you can't, for example, because your class has already 58 | * inherits from one, use it as delegate with [Loggable] interface: 59 | * ~~~kotlin 60 | * class StrangeException(text: String): Exception(text), Loggable by TagLog("SEXC") { 61 | * init { 62 | * debug { "StrangeException has been instantiated" } 63 | * } 64 | * } 65 | * ~~~ 66 | */ 67 | open class LogTag(override var logTag: String, override var logLevel: Log.Level? = null) : Loggable 68 | 69 | /** 70 | * Add log enctry of the specified level. If the log level does not permit message to be logged, its [reporter] 71 | * will not be called at all, giving minimum impact on the production system or like. 72 | * 73 | * Usually, this method is not directly called, instead, use one of [info], [debug], [warning], [error] and 74 | * [exception]. 75 | * 76 | * __Important note__ the reporter could be called in a separated thread, the logger preserves creation time as 77 | * close as it can but perform formatting/collection or wharever else `reporter()` does in a separate coroutine. So 78 | * if you really need some varying value or function result to be captured when reporting, save it in a local variable 79 | * before calling `addLog` and use its captured value inside. 80 | * 81 | * @param level level of this message 82 | * @param reporter function that creates actual log record when needed. 83 | */ 84 | fun Loggable.addLog(level: Log.Level, reporter: () -> LogData) { 85 | if ((logLevel ?: Log.defaultLevel).priority <= level.priority) 86 | Log.add(level, reporter) 87 | } 88 | 89 | /** 90 | * Conditionally emits a [Log.Level.INFO] - level log message. See [addLog] for details. 91 | */ 92 | fun Loggable.info(reporter: () -> String) { 93 | addLog(Log.Level.INFO) { LogData.Message(logTag, reporter()) } 94 | } 95 | 96 | /** 97 | * Conditionally emits a [Log.Level.DEBUG] - level log message. See [addLog] for details. 98 | */ 99 | fun Loggable.debug(reporter: () -> String) { 100 | addLog(Log.Level.DEBUG) { LogData.Message(logTag, reporter()) } 101 | } 102 | 103 | /** 104 | * Conditionally emits a [Log.Level.WARNING] - level log message. See [addLog] for details. 105 | */ 106 | fun Loggable.warning(reporter: () -> String) { 107 | addLog(Log.Level.WARNING) { LogData.Message(logTag, reporter()) } 108 | } 109 | 110 | /** 111 | * Conditionally emits a [Log.Level.ERROR] - level log message. See [addLog] for details. 112 | */ 113 | fun Loggable.error(reporter: () -> String) { 114 | addLog(Log.Level.ERROR) { LogData.Error(logTag, reporter()) } 115 | } 116 | 117 | /** 118 | * Conditionally emits a [Log.Level.ERROR] - level log message adn attatch a throwable excetion object to it, 119 | * with its stack trace in particular. Reported function must return a [Pair] where first value is a log message 120 | * and the second is an exception instance. If tiy don't need one, use [error] instead. See [addLog] for details. 121 | */ 122 | fun Loggable.exception(reporter: () -> Pair) { 123 | addLog(Log.Level.ERROR) { 124 | val (message, exception) = reporter() 125 | LogData.Error(logTag, "$message:${exception.message}", exception.stackTraceToString()) 126 | } 127 | } 128 | 129 | /** 130 | * The logged data serializable container. 131 | */ 132 | @Serializable 133 | sealed class LogData { 134 | /** 135 | * [Loggable#logTag] of the message 136 | */ 137 | abstract val tag: String 138 | 139 | /** 140 | * The log message itself 141 | */ 142 | abstract val message: String 143 | 144 | /** 145 | * The regular log message: no additional data 146 | */ 147 | @Serializable 148 | @SerialName("msg") 149 | class Message(override val tag: String, override val message: String) : LogData() { 150 | override fun toString(): String = message 151 | } 152 | 153 | /** 154 | * Error log message: also an optional stack. 155 | */ 156 | @Serializable 157 | @SerialName("err") 158 | class Error( 159 | override val tag: String, 160 | override val message: String, 161 | val stack: String? = null 162 | ) : LogData() { 163 | override fun toString(): String = "$message${stack?.let { "\n$it" } ?: ""}" 164 | } 165 | } 166 | 167 | /** 168 | * Log entry savesa log level and creation timestamp, as its actual consumption by subscribers (see [Log.logFlow]) 169 | * happens later, sometimes considerably. 170 | */ 171 | @Serializable 172 | data class LogEntry(val level: Log.Level, val data: LogData, val timestamp: Instant) { 173 | override fun toString(): String = "%tT %c %-5s %s".sprintf(timestamp, level.name[0], data.tag, data) 174 | } 175 | 176 | /** 177 | * Shared log state and tools. Use [Loggable] to emit log entries, [logFlow] to collect them (there is also replay 178 | * buffer) and service functions such as [connectConsole] to simplify logging. 179 | */ 180 | object Log { 181 | 182 | /** 183 | * The defaul log level. Attempts to emit messages with a level less than this one will be ignoring without 184 | * evaluating message preparing code. This behavior could be overridden with [Loggable.logLevel] variable. 185 | */ 186 | var defaultLevel: Level = Level.INFO 187 | 188 | enum class Level(val priority: Int) { 189 | HIDDEN(0), 190 | DEBUG(10), 191 | INFO(100), 192 | WARNING(1000), 193 | ERROR(10000) 194 | } 195 | 196 | private val log = MutableSharedFlow(1000, onBufferOverflow = BufferOverflow.DROP_OLDEST) 197 | 198 | /** 199 | * The log entries source. It has some replay buffer so new collectors will receive latest log messages. The flow 200 | * has a limited buffer and drops oldest messages. 201 | */ 202 | val logFlow = log.asSharedFlow() 203 | 204 | /** 205 | * Add a log entry Important! this method __does not checks and filters the level__ allowing even low priority 206 | * messages to be added to the log. This is done intentionally to allow per-instance level filtering. See [Loggable] 207 | * extension functions to get a proper filtered log emission function. 208 | */ 209 | fun add(level: Level, reporter: suspend () -> LogData) { 210 | // We want to stamp time close to report time, launch could be considerably post current time 211 | // and collecting log data could add even more: 212 | val instant = Clock.System.now() 213 | 214 | GlobalScope.launch(sequential) { 215 | log.emit(LogEntry(level, reporter(), instant)) 216 | } 217 | } 218 | 219 | private val sequential = Dispatchers.Default.limitedParallelism(1) 220 | 221 | /** 222 | * Launch a block in the non-concurent loggin dispatcher. Use it as an alternative to a muted, but _only when 223 | * implementing functions closely related to the logging subsystem_. Do not block it! 224 | */ 225 | fun launchExclusive(block: suspend () -> Unit) { 226 | GlobalScope.launch(sequential) { block() } 227 | } 228 | 229 | private var consoleJob: Job? = null 230 | private var stopConsole: Boolean = false 231 | 232 | /** 233 | * If console logger is connected via [connectConsole], this will change it filtering level. Default is stored 234 | * or overriden when console is connected 235 | */ 236 | var consoleLogLevel = Level.DEBUG 237 | 238 | /** 239 | * Start (if not already started) emitting log messages to the console (stdout). Due to replay buffer of the log 240 | * flow, it will immediately emit buffered entries, if any. Repeated calls to it do nothing. 241 | * @param level if set, overrides current value of [consoleLogLevel] thus filtering only messages with a given 242 | * priority and higher. 243 | */ 244 | fun connectConsole(level: Level? = null) { 245 | level?.let { 246 | consoleLogLevel = it 247 | defaultLevel = it 248 | } 249 | launchExclusive { 250 | if (consoleJob == null) { 251 | ConsoleLoggerSetup() 252 | stopConsole = false 253 | val lf = LogFormatter() 254 | consoleJob = GlobalScope.launch(Dispatchers.Unconfined) { 255 | logFlow.collect { record -> 256 | if( stopConsole ) cancel() 257 | if( record.level >= consoleLogLevel ) { 258 | try { 259 | val text = lf.format(record).joinToString("\n") 260 | // if we inline it strange bug eats it 261 | println(text) 262 | } catch (e: Throwable) { 263 | println("***** unexpected logger exception: $e") 264 | e.printStackTrace() 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | 273 | /** 274 | * Stop emitting log messages to the stdount. First call to it immediately stops console output no matter 275 | * how many times [connectConsole] was called before. This behavior though might be altered in future. 276 | */ 277 | suspend fun disconnectConsole() { 278 | withContext(sequential) { 279 | consoleJob?.let { 280 | // Let all emitters a chance to fill the log flows 281 | yield() 282 | // poison pill the console logger 283 | stopConsole = true 284 | add(Level.HIDDEN, { LogData.Message("CONS", "Shutting down console logger") }) 285 | // wait it to die 286 | it.join() 287 | consoleJob = null 288 | } 289 | } 290 | } 291 | } 292 | 293 | expect internal fun ConsoleLoggerSetup() 294 | 295 | @Suppress("unused") 296 | fun Loggable.ignoreExceptions(from: String?=null, f:()->T): Result { 297 | return try { Result.success(f()) } 298 | catch(x: Throwable) { 299 | exception { "${from ?: this::class.simpleName ?: "?"}: exception thrown: $x" to x } 300 | Result.failure(x) 301 | } 302 | } 303 | 304 | @Suppress("unused") 305 | suspend fun Loggable.ignoreAsyncExceptions(from: String?=null,f: suspend ()->T): Result { 306 | return try { Result.success(f()) } 307 | catch(x: Throwable) { 308 | exception { "${from ?: this::class.simpleName ?: "?"}: exception thrown: $x" to x } 309 | Result.failure(x) 310 | } 311 | } 312 | 313 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/AsyncBouncer.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_tools 2 | 3 | import kotlinx.coroutines.Job 4 | import kotlinx.coroutines.TimeoutCancellationException 5 | import kotlinx.coroutines.channels.BufferOverflow 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.withTimeout 9 | import kotlinx.datetime.Instant 10 | import net.sergeych.mptools.Now 11 | import net.sergeych.mptools.withReentrantLock 12 | import kotlin.time.Duration 13 | 14 | /** 15 | * Expermiental multiplatform coroutine-based bouncer: safe way to call some suspend code 16 | * after a timeout. It is thread-safe (where multithreaded) and coroutine-safe. 17 | * 18 | * Note that creating a bouncer will not invoke its callback until the corresponding pulse call. 19 | * 20 | * @param initialTimeout default timeout for [pulse], could be changed at runtime assigning to [timeout]. 21 | * @param initialMaxTimeout maximum timeout between calls, systemm will invoke callback when it expires even if there 22 | * will be [pulse] calls in between. Could be changed with [maxTimeout] 23 | * @param callback what to invoke. 24 | */ 25 | class AsyncBouncer( 26 | initialTimeout: Duration, 27 | initialMaxTimeout: Duration = initialTimeout, 28 | callback: suspend () -> Unit, 29 | ) { 30 | 31 | private var lastCallAt: Instant? = null 32 | private var callAt: Instant? = null 33 | private val access = Mutex() 34 | private val pulseChannel = Channel(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) 35 | 36 | /** 37 | * Default time between call to [pulse] and invocation. Assigning calls [pulse]. 38 | */ 39 | var timeout: Duration = initialTimeout 40 | set(value) { 41 | field = value 42 | pulse() 43 | } 44 | 45 | /** 46 | * Maximim time between invocations: even if [pulse] is being called more often, invocations will happen at this 47 | * rate. Assigning it calls [pulse] 48 | */ 49 | var maxTimeout: Duration = initialMaxTimeout 50 | set(value) { 51 | field = value 52 | pulse() 53 | } 54 | 55 | /** 56 | * Cause a block to be scheduled after [timeout] or right now from now even it is already being executing. 57 | * To change effective [timeout], assign a value to it _prior to call pulse_. 58 | */ 59 | fun pulse(now: Boolean = false) { 60 | checkNotClosed() 61 | globalLaunch { 62 | access.withReentrantLock { 63 | callAt = if (now) Now() else Now() + timeout 64 | lastCallAt?.let { 65 | val limitTime = it + maxTimeout 66 | if (callAt!! > limitTime) callAt = limitTime 67 | } 68 | pulseChannel.send(1) 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Perform a callback exclusively, e.g. nouncer callback is guatemteed not to be active while 75 | * callback is performed, then pulse bouncer() 76 | */ 77 | suspend fun performAndPulse(now: Boolean = false, block: suspend () -> Unit) { 78 | access.withReentrantLock { 79 | checkNotClosed() 80 | block() 81 | pulse(now) 82 | } 83 | } 84 | 85 | private fun checkNotClosed() { 86 | if (isClosed) throw IllegalStateException("Bounced is closed") 87 | } 88 | 89 | private var job: Job? = null 90 | private var stop = false 91 | 92 | val isClosed: Boolean get() = job == null 93 | 94 | /** 95 | * Safely close the bouncer freeing its performer coroutine: if it is being executed or scheduled to execute 96 | * (pulsed) it will perform the block and wait for it to finish. Calling it on closed bounced has 97 | * no effect. 98 | */ 99 | suspend fun close() { 100 | access.withReentrantLock { 101 | if (job != null) { 102 | stop = true 103 | pulse(true) 104 | } 105 | } 106 | job?.join() 107 | job = null 108 | } 109 | 110 | 111 | init { 112 | job = globalLaunch { 113 | do { 114 | val duration = callAt?.let { it - Now() } ?: Duration.INFINITE 115 | try { 116 | if (duration > Duration.ZERO) { 117 | withTimeout(duration) { 118 | pulseChannel.receive() 119 | } 120 | } 121 | } catch (_: TimeoutCancellationException) { 122 | // expected 123 | } 124 | access.withReentrantLock { 125 | if (callAt?.let { it <= Now() } == true) { 126 | callAt = null 127 | try { 128 | callback() 129 | callAt = null 130 | lastCallAt = Now() 131 | } catch (t: Throwable) { 132 | // we can't use logging here as logger uses us ;) 133 | println("unexpected error in AsyncBouncer: $t") 134 | t.printStackTrace() 135 | } 136 | } 137 | } 138 | } while (!stop) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/ByteArrayTools.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalUnsignedTypes::class) 2 | @file:Suppress("unused") 3 | 4 | package net.sergeych.mp_tools 5 | 6 | import kotlin.math.max 7 | 8 | /** 9 | * Decode 4-byte signed integer at the specified offset in big-endian mode. Compatible with `ByteBuffer.getInt` 10 | * with default `BIG_ENDIAN` mode. 11 | */ 12 | fun ByteArray.getInt(at: Int): Int { 13 | var offset = at 14 | var result = 0 15 | 16 | result = (result shl 8) or this[offset++].toUByte().toInt() 17 | result = (result shl 8) or this[offset++].toUByte().toInt() 18 | result = (result shl 8) or this[offset++].toUByte().toInt() 19 | result = (result shl 8) or this[offset].toUByte().toInt() 20 | 21 | return result 22 | } 23 | 24 | /** 25 | * Encode and put 4-byte signed integer at the specified offset in big-endian mode. Compatible with `ByteBuffer.putInt` 26 | * with default `BIG_ENDIAN` mode. 27 | */ 28 | fun ByteArray.putInt(at: Int,value: Int) { 29 | var offset = at+3 30 | var x = value 31 | 32 | for( i in 0..3) { 33 | this[offset--] = (x and 0xFF).toByte() 34 | x = x shr 8 35 | } 36 | } 37 | 38 | /** 39 | * Find first occurrence of a binary substring in this binary array. Uses fast Boyer-Moore algorithm. 40 | */ 41 | fun ByteArray.indexOf(needle: ByteArray) = indexOf(toUByteArray(), needle.toUByteArray()) 42 | 43 | /** 44 | * Find first occurrence of a binary substring in this binary array. Uses fast Boyer-Moore algorithm. 45 | */ 46 | fun ByteArray.indexOf(needle: String) = indexOf(toUByteArray(), needle.encodeToByteArray().toUByteArray()) 47 | 48 | /** 49 | * Search the data array for the first occurrence of the 50 | * specified subarray and return its index or -1 if not found. 51 | * 52 | * There is no Galil because it only generates one match. 53 | * 54 | * @param haystack The data to be scanned 55 | * @param needle The string to search 56 | * @param offset offset to start search from 57 | * @return The start index of the substring 58 | */ 59 | fun indexOf(haystack: UByteArray, needle: UByteArray,offset: UInt=0u): Int { 60 | if (needle.size == 0) { 61 | return 0 62 | } 63 | val charTable = makeCharTable(needle) 64 | val offsetTable = makeOffsetTable(needle) 65 | var i = needle.size - 1 + offset.toInt() 66 | var j: Int 67 | while (i < haystack.size) { 68 | j = needle.size - 1 69 | while (needle[j] == haystack[i]) { 70 | if (j == 0) { 71 | return i 72 | } 73 | --i 74 | --j 75 | } 76 | // i += needle.length - j; // For naive method 77 | i += max(offsetTable[needle.size - 1 - j], charTable[haystack[i].toInt()]) 78 | } 79 | return -1 80 | } 81 | 82 | /** 83 | * Makes the jump table based on the mismatched character information. 84 | */ 85 | private fun makeCharTable(needle: UByteArray): IntArray { 86 | val ALPHABET_SIZE: Int = UByte.MAX_VALUE.toInt() + 1 87 | val table = IntArray(ALPHABET_SIZE) 88 | for (i in table.indices) { 89 | table[i] = needle.size 90 | } 91 | for (i in needle.indices) { 92 | table[needle[i].toInt()] = needle.size - 1 - i 93 | } 94 | return table 95 | } 96 | 97 | /** 98 | * Makes the jump table based on the scan offset which mismatch occurs. 99 | * (bad character rule). 100 | */ 101 | private fun makeOffsetTable(needle: UByteArray): IntArray { 102 | val table = IntArray(needle.size) 103 | var lastPrefixPosition = needle.size 104 | for (i in needle.size downTo 1) { 105 | if (isPrefix(needle, i)) { 106 | lastPrefixPosition = i 107 | } 108 | table[needle.size - i] = lastPrefixPosition - i + needle.size 109 | } 110 | for (i in 0 until needle.size - 1) { 111 | val slen = suffixLength(needle, i) 112 | table[slen] = needle.size - 1 - i + slen 113 | } 114 | return table 115 | } 116 | 117 | /** 118 | * Is needle[p:end] a prefix of needle? 119 | */ 120 | private fun isPrefix(needle: UByteArray, p: Int): Boolean { 121 | var i = p 122 | var j = 0 123 | while (i < needle.size) { 124 | if (needle[i] != needle[j]) { 125 | return false 126 | } 127 | ++i 128 | ++j 129 | } 130 | return true 131 | } 132 | 133 | /** 134 | * Returns the maximum length of the substring ends at p and is a suffix. 135 | * (good suffix rule) 136 | */ 137 | private fun suffixLength(needle: UByteArray, p: Int): Int { 138 | var len = 0 139 | var i = p 140 | var j = needle.size - 1 141 | while (i >= 0 && needle[i] == needle[j]) { 142 | len += 1 143 | --i 144 | --j 145 | } 146 | return len 147 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/CachedExpression.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalTime::class) 2 | 3 | package net.sergeych.mptools 4 | 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.datetime.Clock 7 | import kotlinx.datetime.Instant 8 | import kotlin.time.Duration 9 | import kotlin.time.ExperimentalTime 10 | 11 | /** 12 | * The expression that will be calculated then cached fr specified amount of time. Differs from lazy values 13 | * as the calculation can use dynamic data passed via closures, so it can be recalculated with time or 14 | * by request. Optimized to be used with coroutines, e.g. suspending expression like loading values from network, 15 | * etc. 16 | * 17 | * Usage sample 18 | * 19 | * ~~~kotlin 20 | * val x = CachedExpression(50.seconds) 21 | * //... 22 | * val currentValue = x.get { 23 | * // this will be called once 50 seconds at maximum: 24 | * getValueFromWeb("https://acme.com/api/status") 25 | * } 26 | * ~~~ 27 | * 28 | * Note that expression closure __is not stored and is only executed where and when it was called__ so it is safe 29 | * to use any disposable/recycled resources in it, like database connections, etc. Expression producing block 30 | * will be called before [get] returns, or will not be called at all. Cached is the value, not the producing lambda. 31 | */ 32 | class CachedExpression( 33 | /** 34 | * If not null, recalculated cached expression is automaticallu invalidated after this time. Set it to null 35 | * to keep it indefinitely until [clearCache] is called 36 | */ 37 | var expiresIn: Duration? = null, 38 | initialValue: T? = null, 39 | ) { 40 | 41 | private var cachedValue: T? = initialValue 42 | private var cacheSetAt: Instant? = initialValue?.let { Clock.System.now() } 43 | private val mutex = Mutex() 44 | 45 | /** 46 | * If there is a cached value it will be dropped 47 | */ 48 | suspend fun clearCache() { 49 | mutex.withReentrantLock { cachedValue = null } 50 | } 51 | 52 | @Suppress("unused") 53 | suspend fun overrideCacheWith(value: T) { 54 | mutex.withReentrantLock { cachedValue = value; cacheSetAt = Clock.System.now() } 55 | } 56 | 57 | /** 58 | * Return cache value, if presented and not expired. See [expiresIn]. 59 | */ 60 | suspend fun cachedOrNull(): T? = mutex.withReentrantLock { 61 | if (cachedValue != null) { 62 | expiresIn?.let { d -> 63 | val setAt = cacheSetAt ?: throw IllegalStateException("cached value is set but cacheSetAt is null") 64 | if (setAt + d < Clock.System.now()) 65 | cachedValue = null 66 | } 67 | } 68 | cachedValue 69 | } 70 | 71 | 72 | /** 73 | * Return cached value if exists and not expired, or recalculates new one and caches it. [producer] will either 74 | * be called _before_ return, or not be called at all, so it is safe to use dusposable resource in it. 75 | * @param producer lambda expresson to calculate actual value, that will be cached for subsequent calls 76 | */ 77 | suspend fun get(producer: suspend () -> T) = mutex.withReentrantLock { 78 | cachedOrNull() ?: producer().also { 79 | cacheSetAt = Clock.System.now() 80 | cachedValue = it 81 | } 82 | } 83 | 84 | /** 85 | * Try to get the expression value from the block if it is not already cached. If the block returns 86 | * null, just return it. 87 | */ 88 | suspend fun optGet(producer: suspend () -> T?) = mutex.withReentrantLock { 89 | cachedOrNull() ?: producer().also { 90 | if (it != null) { 91 | cacheSetAt = Clock.System.now() 92 | cachedValue = it 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/CachedRefreshingValue.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(DelicateCoroutinesApi::class) 2 | 3 | package net.sergeych.mp_tools 4 | 5 | import kotlinx.coroutines.* 6 | import kotlinx.datetime.Clock 7 | import kotlinx.datetime.Instant 8 | import net.sergeych.mp_logger.LogTag 9 | import net.sergeych.mp_logger.exception 10 | import net.sergeych.mp_logger.info 11 | 12 | /** 13 | * The value that is periodically refreshed with a provided lambda and return last successful result. Unlike 14 | * [CachedExpression] it will be refreshed even if not used, but there will be a fresh (or best attempt) value 15 | * any time. 16 | * 17 | * __Important! Lambda is called periodically and asynchronously, so do not use any disposable data 18 | * frin a calling closure!__ This is especially important with resources like database connections: if yo 19 | * need one, request and release it inside the refresher block! 20 | */ 21 | @Suppress("unused") 22 | class CachedRefreshingValue( 23 | /** 24 | * Timeout between successful refreshes 25 | */ 26 | val refreshInMilli: Long , 27 | /** 28 | * Timeout on first and second errors 29 | */ 30 | val errorTimeout1Milli: Long = refreshInMilli, 31 | /** 32 | * Timeout after third and all further errors 33 | */ 34 | val errorTimeout2Milli: Long = errorTimeout1Milli*3, 35 | /** 36 | * The lambda to calculate the value to cache 37 | */ 38 | refresher: suspend () -> T 39 | ): LogTag("CRVAL") { 40 | 41 | /** 42 | * Number of error retries after last successful refresh. ) if the current value is refreshed successfully 43 | */ 44 | var errorRetry: Int = 0 45 | private set 46 | 47 | /** 48 | * When the value was refreshed for the last time 49 | */ 50 | var lastUpdatedAt: Instant? = null 51 | private set 52 | 53 | private var completableDeferred = CompletableDeferred() 54 | 55 | /** 56 | * Get the current state. It suspends only if the [refresher] was not called yet. As refresher 57 | * is called automatically in instance initialization, this method most likely won't suspend. 58 | */ 59 | suspend fun get(): T = completableDeferred.await() 60 | 61 | init { 62 | GlobalScope.launch { 63 | while( true ) { 64 | try { 65 | val result = refresher() 66 | if( completableDeferred.isActive ) 67 | completableDeferred.complete(result) 68 | else 69 | completableDeferred = CompletableDeferred(result) 70 | lastUpdatedAt = Clock.System.now() 71 | errorRetry = 0 72 | delay(refreshInMilli) 73 | } 74 | catch(x: Exception) { 75 | exception { "while recalculating cached value" to x } 76 | delay( if( errorRetry++ < 2) errorTimeout1Milli else errorTimeout2Milli ) 77 | } 78 | } 79 | } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/async_tools.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(DelicateCoroutinesApi::class) 2 | 3 | package net.sergeych.mp_tools 4 | 5 | import kotlinx.coroutines.* 6 | import net.sergeych.mp_logger.LogTag 7 | import net.sergeych.mp_logger.exception 8 | 9 | private val log = LogTag("MPTLS") 10 | 11 | 12 | /** 13 | * Launch a standalone cancellable coroutine using GlobalScope. Eats warnings (where possible) and 14 | * do not report `CancellationException`. Other unhandled exceptions are logged with usual [Log] means. 15 | */ 16 | fun globalLaunch(block: suspend CoroutineScope.()->Unit): Job = 17 | GlobalScope.launch { 18 | try { 19 | block() 20 | } 21 | catch(_: CancellationException) { 22 | // this is OK for global launch 23 | } 24 | catch(t: Throwable) { 25 | log.exception { "unexpected in globalLaunch" to t } 26 | } 27 | } 28 | 29 | 30 | /** 31 | * Calculate block in standalone coroutine and return its deferred result 32 | */ 33 | @Suppress("unused") 34 | fun globalDefer(block: suspend CoroutineScope.() -> T): Deferred = GlobalScope.async { block() } 35 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/base64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_tools 2 | 3 | /* 4 | 5 | Why reinventing the wheel? 6 | 7 | Just because. There is no "good" and "safe" way to do in in browser without importing external libraries or going 8 | async. Because there is no kotlin native version, and getting external native dependencies is what we aere trying 9 | to avoid by all costs. And, finally, if we have to write pure kotlin implementation for js + native, we'd better 10 | use it on JVM too - it is at least not worse than JVM standard library. So here we go, the wheel again ;) 11 | 12 | Though the algorithm is old and very well known and optimized, this version is the kotlin adoption of 13 | https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727, 14 | big thanks to [Egor](https://gist.github.com/enepomnyaschih). 15 | 16 | */ 17 | private val base64codes = arrayOf( 18 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 19 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 20 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63, 21 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255, 22 | 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 23 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 24 | 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 25 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 26 | ) 27 | 28 | private val base64abc = arrayOf( 29 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 30 | "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 31 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", 32 | "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 33 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/" 34 | ) 35 | 36 | private fun getBase64Code(charCode: Int): Int { 37 | if (charCode >= base64codes.size) { 38 | throw Exception("Unable to parse base64 string.") 39 | } 40 | val code = base64codes[charCode] 41 | if (code == 255) { 42 | throw Exception("Unable to parse base64 string.") 43 | } 44 | return code 45 | } 46 | 47 | fun ByteArray.encodeToBase64(): String { 48 | val result = StringBuilder() 49 | val l = size 50 | 51 | fun b(index: Int) = this[index].toInt() and 0xFF 52 | 53 | var i = 2 54 | while (i < l) { 55 | result.append(base64abc[b(i - 2) shr 2]) 56 | result.append(base64abc[((b(i - 2) and 0x03) shl 4) or (b(i - 1) shr 4)]) 57 | result.append(base64abc[((b(i - 1) and 0x0F) shl 2) or (b(i) shr 6)]) 58 | result.append(base64abc[b(i) and 0x3F]) 59 | i += 3 60 | } 61 | 62 | if (i == l + 1) { // 1 octet yet to write 63 | result.append(base64abc[b(i - 2) shr 2]) 64 | result.append(base64abc[(b(i - 2) and 0x03) shl 4]) 65 | result.append("==") 66 | } 67 | if (i == l) { // 2 octets yet to write 68 | result.append(base64abc[b(i - 2) shr 2]) 69 | result.append(base64abc[((b(i - 2) and 0x03) shl 4) or (b(i - 1) shr 4)]) 70 | result.append(base64abc[(b(i - 1) and 0x0F) shl 2]) 71 | result.append("=") 72 | } 73 | return result.toString() 74 | } 75 | 76 | private fun removeAllSpaces(src: String): String { 77 | val result = StringBuilder() 78 | for( ch in src) { 79 | when(ch) { 80 | ' ', '\t', '\n', '\r' -> continue 81 | else -> result.append(ch) 82 | } 83 | } 84 | return result.toString() 85 | } 86 | fun String.decodeBase64(): ByteArray { 87 | val str = removeAllSpaces(this) 88 | if (str.length % 4 != 0) { 89 | throw IllegalArgumentException("Unable to parse base64 string: wrong size") 90 | } 91 | val index = str.indexOf("=") 92 | if (index != -1 && index < str.length - 2) { 93 | throw IllegalArgumentException("Unable to parse base64 string: illegal characters") 94 | } 95 | 96 | val missingOctets = when { 97 | str.endsWith("==") -> 2 98 | str.endsWith("=") -> 1 99 | else -> 0 100 | } 101 | 102 | val result = ByteArray(3 * (str.length / 4)) 103 | 104 | var i = 0 105 | var j = 0 106 | while (i < str.length) { 107 | val buffer: Int = (getBase64Code(str[i].code) shl 18) or 108 | (getBase64Code(str[i + 1].code) shl 12) or 109 | (getBase64Code(str[i + 2].code) shl 6) or 110 | getBase64Code(str[i + 3].code) 111 | result[j] = (buffer shr 16).and(0xFF).toByte() 112 | result[j + 1] = ((buffer shr 8) and 0xFF).toByte() 113 | result[j + 2] = (buffer and 0xFF).toByte() 114 | i += 4 115 | j += 3 116 | } 117 | return result.sliceArray(0 until result.size - missingOctets) 118 | } 119 | 120 | private val reSpaces = Regex("\\s+") 121 | 122 | /** 123 | * Decode compact representation of base64. e.g. with oissibly no trailing '=' fill characters, for example, 124 | * encoded with [ByteArray.encodeToBase64Compact] fun. 125 | */ 126 | fun String.decodeBase64Compact(): ByteArray { 127 | val x = StringBuilder(reSpaces.replace(this, "")) 128 | while( x.length % 4 != 0 ) x.append('=') 129 | return x.toString().decodeBase64() 130 | } 131 | 132 | /** 133 | * Encode to base64 with no spaces and no trailing '=' fill characters, to be decoded with [String.decodeBase64Compact]. 134 | */ 135 | fun ByteArray.encodeToBase64Compact(): String { 136 | val result = encodeToBase64() 137 | var end = result.length-1 138 | while( end > 0 && result[end] == '=') end-- 139 | return result.slice(0..end) 140 | } 141 | 142 | /** 143 | * Url-friendly encoding, as used by Google, Yahoo (the name Y64), etc. [encodeToBase64Compact] 144 | * and substitute `+/` to `-_` respectively. 145 | */ 146 | fun ByteArray.encodeToBase64Url(): String = 147 | encodeToBase64Compact().replace('+','-').replace('/', '_') 148 | 149 | /** 150 | * Decode base64 url encoded binary data. See [encodeToBase64Url] for more information 151 | */ 152 | @Suppress("unused") 153 | fun String.decodeBase64Url(): ByteArray = 154 | replace('-','+').replace('_', '/').decodeBase64Compact() -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/reentrant_mutex.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mptools 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | import kotlinx.coroutines.withContext 6 | import kotlin.coroutines.CoroutineContext 7 | import kotlin.coroutines.coroutineContext 8 | 9 | suspend fun Mutex.withReentrantLock(block: suspend () -> T): T { 10 | val key = ReentrantMutexContextKey(this) 11 | // call block directly when this mutex is already locked in the context 12 | if (coroutineContext[key] != null) return block() 13 | // otherwise add it to the context and lock the mutex 14 | return withContext(ReentrantMutexContextElement(key)) { 15 | withLock { block() } 16 | } 17 | } 18 | 19 | class ReentrantMutexContextElement( 20 | override val key: ReentrantMutexContextKey 21 | ) : CoroutineContext.Element 22 | 23 | data class ReentrantMutexContextKey( 24 | val mutex: Mutex 25 | ) : CoroutineContext.Key -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/text_tools.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_tools 2 | 3 | /** 4 | * If the string is longer that given, replace it's middle part with a unicode ellipsis 5 | * character so the overall length will be [size] 6 | */ 7 | fun String.trimMiddle(size: Int): String { 8 | if (this.length <= size) return this 9 | var l0 = (size - 1) / 2 10 | val l1 = l0 11 | if (l0 + l1 + 1 < size) l0++ 12 | if (l0 + l1 + 1 != size) throw RuntimeException("big in trimMiddle: $size $l0 $l1") 13 | return substring(0, l0) + '…' + substring(length - l1) 14 | } 15 | 16 | /** 17 | * Trim this string as needed and append ellipsis character so the resulting size will be 18 | * no longer than [size] 19 | */ 20 | fun String.trimToEllipsis(size: Int): String { 21 | if (this.length <= size) return this 22 | return (this.substring(0, size - 1)) + '…' 23 | } 24 | 25 | /** 26 | * Convert number to human-readable estimated value in b, Kb, Mb, Gb amd Pb 27 | * to make the nuymber comfortably readable 28 | */ 29 | @Suppress("unused") 30 | fun Number.toDataSize(): String { 31 | var d = toLong() 32 | if (d < 1024) 33 | return "${d}b" 34 | d /= 1024 35 | if (d < 1024) 36 | return "${d}Kb" 37 | d /= 1024 38 | if (d < 1024) 39 | return "${d}Mb" 40 | d /= 1024 41 | if (d < 1024) 42 | return "${d}Gb" 43 | d /= 1024 44 | if (d < 1024) 45 | return "${d}Tb" 46 | d /= 1024 47 | return "${d}Pb" 48 | } 49 | 50 | /** 51 | * Format any number type by separating thousands, millions, etc in _integer part_ using 52 | * a space or other [separator]. 53 | */ 54 | fun Number.withThousandsSeparator(separator: String=" "): String { 55 | val src = toString() 56 | val result = StringBuilder() 57 | var pos = src.indexOf('.') 58 | if( pos >= 0 ) { 59 | result.append(src.substring(pos--)) 60 | } 61 | else pos = src.lastIndex 62 | var count = 0 63 | 64 | while(pos >= 0) { 65 | if( src[pos] == '-') { 66 | result.insert(0, '-') 67 | pos-- 68 | } 69 | else { 70 | if (count++ == 3) { 71 | count = 1 72 | result.insert(0, separator) 73 | } 74 | result.insert(0, src[pos--]) 75 | } 76 | } 77 | return result.toString() 78 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/mp_tools/time_tools_experimental.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mptools 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | 6 | val Instant.isInPast: Boolean get() = 7 | this < Clock.System.now() 8 | 9 | val Instant.isInFuture: Boolean get() = 10 | this > Clock.System.now() 11 | 12 | fun Now(): Instant { 13 | return Clock.System.now() 14 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/sprintf/ExponentFormatter.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlin.math.* 4 | 5 | class ExponentFormatter(val value: Double) { 6 | 7 | var mantissa: Double 8 | private set 9 | var exponent: Int = 0 10 | get() = field 11 | private set(value) { 12 | field = value 13 | strExponent = "e$exponent" 14 | } 15 | 16 | private val mstr: String 17 | private var strExponent: String 18 | 19 | init { 20 | val x = abs(value) 21 | exponent = log10(x).toInt() 22 | if (exponent < 0) exponent-- 23 | mantissa = x / 10.0.pow(exponent) 24 | if (value < 0) mantissa = -mantissa 25 | mstr = mantissa.toString() 26 | strExponent = "e$exponent" 27 | } 28 | 29 | fun scientific(width: Int, fractionWidth: Int = -1): String { 30 | val minLength = if (mantissa < 0) 2 else 1 31 | 32 | // Get the desired part of mantissa with proper bounding and rounding 33 | // it works only for "normalized" mantissa that is always has d[.ddddd] form, e.g. integer 34 | // part is always 1 digit long 35 | // 36 | // ERROR: this rounding does not work with trailint (***9)8 - like variants 37 | // 38 | fun mpart(length: Int): String { 39 | var l = length 40 | if (l > mstr.length) l = mstr.length 41 | val result = StringBuilder(mstr.slice(0 until l)) 42 | if (result.length == mstr.length) 43 | // exact value, no rounding: 44 | return result.toString() 45 | 46 | // last visible digit index 47 | // var lastIndex = result.length - 1 48 | // if (result[lastIndex] == '.') lastIndex-- 49 | 50 | // next significant digit 51 | var nextDigit = mstr[result.length] 52 | if (nextDigit == '.') { 53 | if (result.length + 1 >= mstr.length) 54 | return result.toString() 55 | nextDigit = mstr[result.length + 1] 56 | } 57 | if (nextDigit in "56789") { 58 | val (m, ovf) = roundUp(result) 59 | if( !ovf ) return m 60 | // overflow: exponent should grow 61 | exponent++ 62 | // and the point position should be fixed 63 | val pointPos = m.indexOf('.') 64 | val mb = StringBuilder(m) 65 | if( pointPos == -1 ) 66 | return m // it was the last letter and was tehrefore removed by roundUp 67 | return mb.deleteAt(pointPos).insert(pointPos - 1, '.').toString() 68 | } 69 | return result.toString() 70 | } 71 | 72 | // val mantissa = mstr.toDouble() 73 | 74 | 75 | if (width == 0) return mstr + strExponent 76 | 77 | if (fractionWidth < 0 && width > 0) { 78 | var l = width - strExponent.length 79 | if (l < minLength) l = minLength 80 | return mpart(l) + strExponent 81 | } 82 | 83 | if (fractionWidth < 0 && width < 0) { 84 | return mstr + strExponent 85 | } 86 | 87 | // fractionWidth >= 0 88 | if (fractionWidth == 0) return "${mstr[0]}$strExponent" 89 | 90 | // fractionWitdth > 0, +1 for decimal dot 91 | return mpart(minLength + 1 + fractionWidth) + strExponent 92 | } 93 | 94 | 95 | override fun toString(): String { 96 | return "${mantissa}e${exponent}" 97 | } 98 | 99 | } 100 | 101 | internal fun scientificFormat(value: Double, width: Int, fractionPartLength: Int = -1) = 102 | ExponentFormatter(value).scientific(width, fractionPartLength) 103 | 104 | internal fun fractionalFormat(_value: Double, width: Int, fractionPartLength: Int = -1): String { 105 | var value = _value 106 | val result = StringBuilder() 107 | 108 | if (abs(value) >= 1) { 109 | val i = if (fractionPartLength == 0) value.roundToLong() else value.toLong() 110 | result.append(i) 111 | result.append('.') 112 | value -= i 113 | } else 114 | result.append((if (value < 0) "-0." else "0.")) 115 | 116 | var fl = if (fractionPartLength < 0) { 117 | if (width < 0) 6 118 | else width - result.length 119 | } else fractionPartLength 120 | 121 | var rest = value * 10 122 | while (fl-- > 0) { 123 | val d = rest.toInt() 124 | result.append(abs(d)) 125 | rest = (rest - d) * 10 126 | } 127 | // now we might need to round it up: 128 | return if( rest.toInt().absoluteValue < 5 ) result.toString() else roundUp(result, keepWidth = false).first 129 | } 130 | 131 | /** 132 | * Round up the mantissa part (call it with default arguments to start). 133 | * @return rounded mantissa and overflow flag (set when 9,99 -> 10,00 and like) 134 | */ 135 | private fun roundUp( 136 | result: StringBuilder, 137 | length: Int = result.length, 138 | pos: Int = result.length - 1, 139 | keepWidth: Boolean = true 140 | ): Pair { 141 | if (pos < 0) { 142 | // if we get there, it means the number of digits should grow, like "9.99" -> "10.00" 143 | // but we need to keep the length so "10.0": 144 | result.insert(0, '1') 145 | if( keepWidth ) result.deleteAt(length) 146 | return result.toString() to true 147 | } 148 | // not the first digit: perform rounding: 149 | val d = result[pos] 150 | // it could be a decimal point we ignore and continue with rounding 151 | if (d == '.') return roundUp(result, length, pos - 1, keepWidth) 152 | 153 | // Small number add one "0.19" -> "0.2" 154 | // Simple case: alter only the current digit 155 | if (d != '9') { 156 | result[pos] = d + 1 157 | return result.toString() to false 158 | } 159 | // Complex case: 9->0 and propogate changes up. 160 | result[pos] = '0' 161 | return roundUp(result, length, pos - 1, keepWidth) 162 | } 163 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/sprintf/Specification.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.* 4 | 5 | internal enum class Positioning { 6 | LEFT, RIGHT, CENTER 7 | } 8 | 9 | internal class Specification(val parent: Sprintf, var index: Int) { 10 | 11 | enum class Stage { 12 | FLAGS, 13 | LENGTH, 14 | FRACTION 15 | } 16 | 17 | private var stage = Stage.FLAGS 18 | 19 | private var size: Int = -1 20 | private var fractionalPartSize: Int = -1 21 | private var positioninig = Positioning.RIGHT 22 | private var fillChar = ' ' 23 | private var currentPart = StringBuilder() 24 | 25 | // private var pos = 0 26 | private var explicitPlus = false 27 | private var done = false 28 | private var indexIsOverriden = false 29 | 30 | private val isScanningFlags: Boolean 31 | get() = stage == Stage.FLAGS 32 | 33 | internal fun scan() { 34 | while (!done) { 35 | val ch = parent.nextChar() 36 | // println("spec: $ch: $stage [$currentPart]") 37 | when (ch) { 38 | '-', '^' -> { 39 | if (!isScanningFlags) invalidFormat("unexpected $ch") 40 | positioninig = if (ch == '-') Positioning.LEFT else Positioning.CENTER 41 | } 42 | '+' -> { 43 | if (!isScanningFlags) invalidFormat("unexpected $ch") 44 | explicitPlus = true 45 | } 46 | in "*#_=" -> { 47 | if (!isScanningFlags) invalidFormat("bad fill char $ch position") 48 | fillChar = ch 49 | } 50 | '0' -> { 51 | if (isScanningFlags) fillChar = '0' 52 | else 53 | currentPart.append(ch) 54 | } 55 | in "123456789" -> { 56 | if (stage == Stage.FLAGS) stage = Stage.LENGTH 57 | currentPart.append(ch) 58 | } 59 | '$', '!' -> { 60 | if (stage != Stage.LENGTH) invalidFormat("unexpected $ch position") 61 | if (indexIsOverriden) invalidFormat("argument number '$ch' should occur only once") 62 | indexIsOverriden = true 63 | index = currentPart.toString().toInt() - 1 64 | parent.pushbackArgumentIndex() 65 | currentPart.clear() 66 | } 67 | 's' -> createStringField() 68 | 'd', 'i' -> createIntegerField() 69 | 'o' -> createOctalField() 70 | 'x' -> createHexField(false) 71 | 'X' -> createHexField(true) 72 | 'f', 'F' -> createFloat() 73 | 'E' -> createScientific(true) 74 | 'e' -> createScientific(false) 75 | 'g' -> createAutoFloat(true) 76 | 'G' -> createAutoFloat(false) 77 | 'c', 'C' -> createCharacter() 78 | 't' -> createTimeField(false) 79 | 'T' -> createTimeField(true) 80 | '.' -> { 81 | when (stage) { 82 | Stage.FLAGS -> stage = Stage.FRACTION 83 | Stage.LENGTH -> { 84 | endStage(false) 85 | stage = Stage.FRACTION 86 | } 87 | else -> invalidFormat("can't parse specification: unexpected '.'") 88 | } 89 | } 90 | else -> invalidFormat("unexpected character '$ch'") 91 | } 92 | } 93 | } 94 | 95 | private fun invalidFormat(message: String): Nothing { 96 | parent.invalidFormat(message) 97 | } 98 | 99 | private val time: LocalDateTime 100 | get() = parent.getLocalDateTime(index) 101 | 102 | private fun createTimeField(upperCase: Boolean) { 103 | val ch = parent.nextChar() 104 | endStage() 105 | val result: String = when (ch) { 106 | 'H' -> "%02d".sprintf(time.hour) 107 | 'k' -> "%d".sprintf(time.hour) 108 | 'I', 'l' -> { 109 | var t = time.hour 110 | if (t > 12) t -= 12 111 | if (ch == 'I') "%02d".sprintf(t) 112 | else t.toString() 113 | } 114 | 'M' -> "%02d".sprintf(time.minute) 115 | 'S' -> "%02d".sprintf(time.second) 116 | 'L' -> "%03d".sprintf(time.nanosecond / 1_000_000) 117 | 'N' -> "%09d".sprintf(time.nanosecond) 118 | 'p' -> { 119 | if (upperCase) 120 | if (time.hour > 12) "PM" else "AM" 121 | else 122 | if (time.hour > 12) "pm" else "am" 123 | } 124 | 'z' -> { 125 | val tz = TimeZone.currentSystemDefault() 126 | tz.offsetAt(time.toInstant(tz)).toString().replace(":", "") 127 | } 128 | 'Z' -> { 129 | // There us yet no abbreviations like 'CET', so we put there string representation like +01:00 130 | val tz = TimeZone.currentSystemDefault() 131 | tz.offsetAt(time.toInstant(tz)).toString() 132 | } 133 | 's' -> { 134 | val tz = TimeZone.currentSystemDefault() 135 | time.toInstant(tz).epochSeconds.toString() 136 | } 137 | 'Q' -> { 138 | val tz = TimeZone.currentSystemDefault() 139 | time.toInstant(tz).toEpochMilliseconds().toString() 140 | } 141 | // Date fields 142 | 'B' -> getMonthName(time.month.number) 143 | 'b', 'h' -> getAbbreviatedMonthName(time.month.number) 144 | 'e' -> time.dayOfMonth.toString() 145 | 'd' -> "%02s".sprintf(time.dayOfMonth) 146 | 'm' -> "%02s".sprintf(time.month.number) 147 | 'A' -> getWeekDayName(time.dayOfWeek) 148 | 'a' -> getAbbreviatedWeekDayName(time.dayOfWeek) 149 | 'y' -> time.year.toString().takeLast(2) 150 | 'Y' -> "%04d".sprintf(time.year) 151 | 'j' -> "%03d".sprintf(time.dayOfYear) 152 | // shortcuts 153 | 'R' -> "%1!tH:%1!tM".sprintf(time) 154 | 'r' -> 155 | if (upperCase) 156 | "%1!tI:%1!tM:%1!tS %1!Tp".sprintf(time) 157 | else 158 | "%1!tI:%1!tM:%1!tS %1!tp".sprintf(time) 159 | 'T' -> "%tH:%1!tM:%1!tS".sprintf(time) 160 | 'D' -> "%tm/%1!td/%1!ty".sprintf(time) 161 | 'F' -> "%tY-%1!tm-%1!td".sprintf(time) 162 | 'c' -> "%ta %1!tb %1!td %1!tT %1!tZ %1!tY".sprintf(time) 163 | 'O' -> { 164 | val tz = TimeZone.currentSystemDefault() 165 | val offset = tz.offsetAt(time.toInstant(tz)).toString() 166 | "%tFT%1!tT%s".sprintf(time, offset) 167 | } 168 | '#' -> { 169 | "%tY%1!tm%1!td%1!tH%1!tM%1!tS".sprintf(time.toInstant(TimeZone.UTC)) 170 | } 171 | else -> invalidFormat("unknown time field specificator: 't$ch'") 172 | } 173 | insertField(result) 174 | } 175 | 176 | private fun createStringField() { 177 | endStage() 178 | insertField(parent.getText(index)) 179 | } 180 | 181 | private fun createIntegerField() { 182 | endStage() 183 | val number = parent.getNumber(index).toLong() 184 | // Negative && fill os a special case: 185 | if (number < 0 && fillChar == '0') 186 | insertField((-number).toString(), "-") 187 | else { 188 | 189 | if (explicitPlus && fillChar == '0' && number > 0) 190 | insertField(number.toString(), "+") 191 | else 192 | insertField(if (explicitPlus && number > 0) "+$number" else "$number") 193 | } 194 | } 195 | 196 | private fun createHexField(upperCase: Boolean) { 197 | endStage() 198 | if (explicitPlus) invalidFormat("'+' is incompatible with hex format") 199 | val src = parent.notNull(index) 200 | val n = (src as? Number)?.toLong() ?: throw IllegalArgumentException("can't treat '$src' as integer number") 201 | val text = if( n >= 0 ) 202 | n.toString(16) 203 | else { 204 | val n2 = n.toULong() 205 | val t = when { 206 | src is Byte -> (n2 and 0xFFu).toString(16) 207 | src is Short -> (n2 and 0xFFFFu).toString(16) 208 | src is Int -> (n2 and 0xFFFFFFFFu).toString(16) 209 | else -> n2.toString(16) 210 | } 211 | if( size >=- 0) t.take(size) else t 212 | } 213 | insertField(if (upperCase) text.uppercase() else text.lowercase()) 214 | } 215 | 216 | private fun createOctalField() { 217 | endStage() 218 | val number = parent.getNumber(index).toLong() 219 | if (explicitPlus) invalidFormat("'+' is incompatible with oct format") 220 | insertField(number.toString(8)) 221 | } 222 | 223 | private fun createCharacter() { 224 | endStage() 225 | insertField(parent.getCharacter(index).toString()) 226 | } 227 | 228 | private fun endStage(setDone: Boolean = true) { 229 | if (setDone) done = true 230 | if (currentPart.isNotEmpty()) { 231 | when (stage) { 232 | Stage.LENGTH -> size = currentPart.toString().toInt() 233 | Stage.FRACTION -> fractionalPartSize = currentPart.toString().toInt() 234 | Stage.FLAGS -> invalidFormat("can't parse format specifier (error 7)") 235 | } 236 | currentPart.clear() 237 | } 238 | } 239 | 240 | private fun insertField(text: String, prefix: String = "") { 241 | val l = text.length + prefix.length 242 | if (size < 0 || size < l) { 243 | parent.specificationDone(prefix + text) 244 | } else { 245 | var padStart = 0 246 | var padEnd = 0 247 | when (positioninig) { 248 | Positioning.LEFT -> padEnd = size - l 249 | Positioning.RIGHT -> padStart = size - l 250 | Positioning.CENTER -> { 251 | padStart = (size - l) / 2 252 | padEnd = size - padStart - l 253 | 254 | } 255 | } 256 | val result = StringBuilder(prefix) 257 | while (padStart-- > 0) result.append(fillChar) 258 | result.append(text) 259 | while (padEnd-- > 0) result.append(fillChar) 260 | parent.specificationDone(result.toString()) 261 | } 262 | } 263 | 264 | private fun createFloat() { 265 | endStage() 266 | val number = parent.getNumber(index).toDouble() 267 | val t = fractionalFormat(number, size, fractionalPartSize) 268 | 269 | if (explicitPlus && fillChar == '0' && number > 0) 270 | insertField(t, "+") 271 | else 272 | insertField(if (explicitPlus && number > 0) "+$t" else t) 273 | } 274 | 275 | private fun createScientific(upperCase: Boolean) { 276 | endStage() 277 | val number = parent.getNumber(index).toDouble() 278 | val t = scientificFormat(number, size, fractionalPartSize).let { 279 | if (upperCase) it.uppercase() else it.lowercase() 280 | } 281 | 282 | if (explicitPlus && fillChar == '0' && number > 0) 283 | insertField(t, "+") 284 | else 285 | insertField(if (explicitPlus && number > 0) "+$t" else t) 286 | } 287 | 288 | private fun createAutoFloat(upperCase: Boolean) { 289 | endStage() 290 | val number = parent.getNumber(index) 291 | val t = number.toString().let { 292 | if (upperCase) it.uppercase() else it.lowercase() 293 | } 294 | 295 | if (explicitPlus && fillChar == '0' && number.toDouble() > 0) 296 | insertField(t, "+") 297 | else 298 | insertField(if (explicitPlus && number.toFloat() > 0) "+$t" else t) 299 | } 300 | 301 | companion object { 302 | val englishMonthNames: List by lazy { 303 | "January February March April May June July August September October November December".split( 304 | ' ' 305 | ) 306 | } 307 | val englishWeekDayNames: List by lazy { 308 | "Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(' ') 309 | } 310 | 311 | fun getAbbreviatedMonthName(monthNumber: Int) = 312 | LocaleSpecificAbbreviatedMonthName(monthNumber) ?: getMonthName(monthNumber).take(3) 313 | 314 | fun getMonthName(monthNumber: Int) = LocaleSpecificMonthName(monthNumber) ?: englishMonthNames[monthNumber - 1] 315 | 316 | fun getWeekDayName(d: DayOfWeek): String { 317 | val n = d.isoDayNumber 318 | return LocaleSpecificDayName(n) ?: englishWeekDayNames[n-1] 319 | } 320 | 321 | fun getAbbreviatedWeekDayName(d: DayOfWeek): String { 322 | val n = d.isoDayNumber 323 | return LocaleSpecificAbbreviatedDayName(n) ?: englishWeekDayNames[n-1].take(3) 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * Platform could provide current locale based month name or return null to use English. Month number is 1..12 330 | * as default in date operations in java 331 | */ 332 | expect fun LocaleSpecificMonthName(monthNumber: Int): String? 333 | 334 | expect fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? 335 | 336 | expect fun LocaleSpecificDayName(isoDayNumber: Int): String? 337 | 338 | expect fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? 339 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/net/sergeych/sprintf/Sprintf.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.* 4 | 5 | internal class Sprintf(val format: String, val args: Array) { 6 | 7 | private var pos = 0 8 | private var specStart = -1 9 | private val result = StringBuilder() 10 | private var currentIndex = 0 11 | 12 | fun process(): Sprintf { 13 | while (pos < format.length) { 14 | val ch = format[pos++] 15 | // println("$ch $pos $specStart [$result]") 16 | if (ch == '%') { 17 | when { 18 | specStart == pos - 1 -> { 19 | result.append(ch) 20 | specStart = -1 21 | } 22 | specStart < 0 -> specStart = pos 23 | else -> invalidFormat("unexpected %") 24 | } 25 | } else { 26 | if (specStart >= 0) { 27 | pos-- 28 | Specification(this, currentIndex++).scan() 29 | } else result.append(ch) 30 | } 31 | } 32 | return this 33 | } 34 | 35 | internal fun nextChar(): Char { 36 | if (pos >= format.length) invalidFormat("unexpected end of string inside format specification") 37 | return format[pos++] 38 | } 39 | 40 | internal fun invalidFormat(reason: String): Nothing { 41 | throw IllegalArgumentException("bad format: $reason at ofset ${pos - 1} of \"$format\"") 42 | } 43 | 44 | override fun toString(): String = result.toString() 45 | 46 | internal fun getNumber(index: Int): Number { 47 | return notNullArg(index) 48 | } 49 | 50 | internal fun getText(index: Int): String { 51 | return args[index]!!.toString() 52 | } 53 | 54 | internal fun getCharacter(index: Int): Char { 55 | return notNullArg(index) 56 | } 57 | 58 | internal fun specificationDone(text: String) { 59 | result.append(text) 60 | specStart = -1 61 | } 62 | 63 | fun getLocalDateTime(index: Int): LocalDateTime { 64 | val t = notNullArg(index) 65 | return when(t) { 66 | is Instant -> t.toLocalDateTime(TimeZone.currentSystemDefault()) 67 | is LocalDateTime -> t 68 | is LocalDate -> t.atTime(0,0,0) 69 | else -> ConvertToInstant(t).toLocalDateTime(TimeZone.currentSystemDefault()) 70 | } 71 | } 72 | 73 | @Suppress("UNCHECKED_CAST") 74 | fun notNullArg(index: Int) = args[index]!! as T 75 | 76 | fun notNull(index: Int) = args[index]!! 77 | 78 | fun pushbackArgumentIndex() { 79 | currentIndex-- 80 | } 81 | } 82 | 83 | /** 84 | * Old good C-sprintf. See formats table in the 85 | * [readme](https://github.com/sergeych/mp_stools/blob/263cfc50f23cfde815928c7bdb748857fdaad2b0/README.md#L56-L56) 86 | */ 87 | fun String.sprintf(vararg args: Any?): String = Sprintf(this, args).process().toString() 88 | 89 | /** 90 | * Old good C-printf. See formats table in readme. Just like C version it does not insert \n. See also 91 | * [sprintf] 92 | */ 93 | fun printf(format: String,vararg args: Any?) { 94 | print(Sprintf(format, args).process().toString()) 95 | } 96 | 97 | /** 98 | * Like old good C printf but also adds new line (via println). See [sprintf] 99 | */ 100 | @Suppress("unused") 101 | fun printlnf(format: String, vararg args: Any?) { 102 | println(Sprintf(format, args).process().toString()) 103 | } 104 | 105 | 106 | 107 | fun String.format(vararg args: Any?): String = Sprintf(this, args).process().toString() 108 | 109 | expect fun ConvertToInstant(t: Any): Instant -------------------------------------------------------------------------------- /src/commonTest/kotlin/sprintf/SprintfTest.kt: -------------------------------------------------------------------------------- 1 | package sprintf 2 | 3 | import kotlinx.datetime.* 4 | import net.sergeych.sprintf.* 5 | import net.sergeych.sprintf.fractionalFormat 6 | import net.sergeych.sprintf.scientificFormat 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertTrue 10 | 11 | internal class SprintfTest { 12 | 13 | @Test 14 | fun testSimpleFormat() { 15 | assertEquals("hello", "hello".sprintf()) 16 | assertEquals("% percent", "%% percent".sprintf()) 17 | assertEquals("10%", "10%%".sprintf()) 18 | } 19 | 20 | @Test 21 | fun testIntegers() { 22 | assertEquals("1 2 3", "%d %d %d".sprintf(1,2,3)) 23 | 24 | assertEquals("== 3 ==","== %d ==".sprintf(3)) 25 | assertEquals("== 3 ==","== %3d ==".sprintf(3)) 26 | assertEquals("== 003 ==","== %03d ==".sprintf(3)) 27 | assertEquals("== 3 ==","== %-3d ==".sprintf(3)) 28 | assertEquals("== 3 ==","== %^3d ==".sprintf(3)) 29 | assertEquals("== **3** ==","== %*^5d ==".sprintf(3)) 30 | assertEquals("== __3__ ==","== %_^5d ==".sprintf(3)) 31 | 32 | assertEquals("== +3 ==","== %+d ==".sprintf(3)) 33 | assertEquals("== +3 ==","== %+5d ==".sprintf(3)) 34 | assertEquals("== +0003 ==","== %+05d ==".sprintf(3)) 35 | 36 | assertEquals("== -3 ==","== %+d ==".sprintf(-3)) 37 | assertEquals("== -3 ==","== %+5d ==".sprintf(-3)) 38 | assertEquals("== -0003 ==","== %+05d ==".sprintf(-3)) 39 | assertEquals("== -0003 ==","== %05d ==".sprintf(-3)) 40 | 41 | assertEquals("== 1e ==","== %x ==".sprintf(0x1e)) 42 | assertEquals("== 1E ==","== %X ==".sprintf(0x1e)) 43 | assertEquals("== 0F ==","== %02X ==".sprintf(15)) 44 | assertEquals("== FF ==","== %02X ==".sprintf(-1)) 45 | assertEquals("01 ff", "%02x %02x".sprintf(1, 255)) 46 | 47 | assertEquals("== ###1e ==","== %#5x ==".sprintf(0x1e)) 48 | assertEquals("== 1e### ==","== %#-5x ==".sprintf(0x1e)) 49 | assertEquals("== ##1E## ==","== %#^6X ==".sprintf(0x1e)) 50 | } 51 | 52 | @Test 53 | fun testFractionDecimals() { 54 | assertEquals("0.124", fractionalFormat(0.1237, -1, 3)) 55 | assertEquals("0.1237", fractionalFormat(0.1237, -1, 4)) 56 | assertEquals("0.12370", fractionalFormat(0.1237, -1, 5)) 57 | assertEquals("0.123700", fractionalFormat(0.1237, -1, 6)) 58 | assertEquals("0.1", fractionalFormat(0.1237, -1, 1)) 59 | assertEquals("0.12", fractionalFormat(0.1237, 4, -1)) 60 | assertEquals("-0.1", fractionalFormat(-0.1237, 4, -1)) 61 | 62 | assertEquals("-0.124", fractionalFormat(-0.1237, -1, 3)) 63 | assertEquals("-0.1237", fractionalFormat(-0.1237, -1, 4)) 64 | 65 | assertEquals("-221.12", fractionalFormat(-221.1217, -1, 2)) 66 | assertEquals("-221.122", fractionalFormat(-221.1217, -1, 3)) 67 | 68 | assertEquals("221.122", fractionalFormat(221.1217, -1, 3)) 69 | assertEquals("221.1217", fractionalFormat(221.1217, -1, 4)) 70 | 71 | assertEquals("221.122", "%.3f".sprintf(221.1217)) 72 | assertEquals("__221.1", "%_7.1f".sprintf(221.1217)) 73 | assertEquals("_+221.1", "%+_7.1f".sprintf(221.1217)) 74 | assertEquals("+0221.1", "%+07.1f".sprintf(221.1217)) 75 | assertEquals("00221.1", "%07.1f".sprintf(221.1217)) 76 | 77 | assertEquals("1.000", "%.3f".sprintf(1)) 78 | } 79 | 80 | @Test 81 | fun testScientificDecimals() { 82 | val x = ExponentFormatter(-162.345678) 83 | // println(":: $x") 84 | // for( i in 3 .. 15 ) println("${x.value} $i: ${x.scientific(i)}") 85 | fun test(n: Int,expected: String) { 86 | assertEquals(expected, x.scientific(n)) 87 | } 88 | test(3, "-2e2") 89 | test(4, "-2e2") 90 | test(5, "-2.e2") 91 | test(6, "-1.6e2") 92 | test(7, "-1.62e2") 93 | test(8, "-1.623e2") 94 | test(9, "-1.6235e2") 95 | test(10, "-1.62346e2") 96 | test(11, "-1.623457e2") 97 | test(12, "-1.6234568e2") 98 | test(13, "-1.62345678e2") 99 | test(14, "-1.62345678e2") 100 | test(15, "-1.62345678e2") 101 | 102 | assertEquals("2.4e0", scientificFormat(2.39, 5) ) 103 | assertEquals("-2.4e-3", scientificFormat(-2.39e-3, 7) ) 104 | assertEquals("2.4e-3", scientificFormat(2.39e-3, 6) ) 105 | 106 | assertEquals("-2.4e-3", scientificFormat(-2.39e-3, -1, 1) ) 107 | assertEquals("-2.39e-3", scientificFormat(-2.39e-3, -1, 2) ) 108 | 109 | assertEquals("-2.39e-3", "%.2e".sprintf(-2.39e-3, -1, 2) ) 110 | assertEquals("2.4e-3", "%6e".sprintf(2.39e-3)) 111 | 112 | assertEquals("-2.39E-3", "%.2E".sprintf(-2.39e-3) ) 113 | assertEquals("2.39E-3", "%.2E".sprintf(2.39e-3) ) 114 | assertEquals("+2.39E-3", "%+.2E".sprintf(2.39e-3) ) 115 | 116 | assertEquals("2.4E-3", "%6E".sprintf(2.39e-3)) 117 | assertEquals("0002.4E-3", "%09.1E".sprintf(2.39e-3)) 118 | assertEquals("+002.4E-3", "%+09.1E".sprintf(2.39e-3)) 119 | } 120 | 121 | @Test 122 | fun testScientificFormat() { 123 | val x = ExponentFormatter(3.349832285740512) 124 | assertEquals("3.350e0", x.scientific(7)) 125 | 126 | assertEquals("3.350e0", x.scientific(7)) 127 | 128 | assertEquals("9.998e0", ExponentFormatter(9.9980).scientific(7)) 129 | assertEquals("9.998e0", ExponentFormatter(9.9981).scientific(7)) 130 | assertEquals("9.998e0", ExponentFormatter(9.9982).scientific(7)) 131 | assertEquals("9.998e0", ExponentFormatter(9.9983).scientific(7)) 132 | assertEquals("9.998e0", ExponentFormatter(9.9984).scientific(7)) 133 | assertEquals("9.999e0", ExponentFormatter(9.9985).scientific(7)) 134 | assertEquals("9.999e0", ExponentFormatter(9.9986).scientific(7)) 135 | assertEquals("9.999e0", ExponentFormatter(9.9987).scientific(7)) 136 | assertEquals("9.999e0", ExponentFormatter(9.9988).scientific(7)) 137 | assertEquals("9.999e0", ExponentFormatter(9.9989).scientific(7)) 138 | 139 | assertEquals("9.939e0", ExponentFormatter(9.9390).scientific(7)) 140 | assertEquals("9.939e0", ExponentFormatter(9.9391).scientific(7)) 141 | assertEquals("9.939e0", ExponentFormatter(9.9392).scientific(7)) 142 | assertEquals("9.939e0", ExponentFormatter(9.9393).scientific(7)) 143 | assertEquals("9.939e0", ExponentFormatter(9.9394).scientific(7)) 144 | assertEquals("9.940e0", ExponentFormatter(9.9395).scientific(7)) 145 | assertEquals("9.940e0", ExponentFormatter(9.9396).scientific(7)) 146 | assertEquals("9.940e0", ExponentFormatter(9.9397).scientific(7)) 147 | assertEquals("9.940e0", ExponentFormatter(9.9398).scientific(7)) 148 | assertEquals("9.940e0", ExponentFormatter(9.9399).scientific(7)) 149 | 150 | assertEquals("9.399e0", ExponentFormatter(9.3990).scientific(7)) 151 | assertEquals("9.399e0", ExponentFormatter(9.3991).scientific(7)) 152 | assertEquals("9.399e0", ExponentFormatter(9.3992).scientific(7)) 153 | assertEquals("9.399e0", ExponentFormatter(9.3993).scientific(7)) 154 | assertEquals("9.399e0", ExponentFormatter(9.3994).scientific(7)) 155 | assertEquals("9.400e0", ExponentFormatter(9.3995).scientific(7)) 156 | assertEquals("9.400e0", ExponentFormatter(9.3995).scientific(7)) 157 | assertEquals("9.400e0", ExponentFormatter(9.3996).scientific(7)) 158 | assertEquals("9.400e0", ExponentFormatter(9.3997).scientific(7)) 159 | assertEquals("9.400e0", ExponentFormatter(9.3998).scientific(7)) 160 | assertEquals("9.400e0", ExponentFormatter(9.3999).scientific(7)) 161 | 162 | assertEquals("9.999e0", ExponentFormatter(9.9990).scientific(7)) 163 | assertEquals("9.999e0", ExponentFormatter(9.9991).scientific(7)) 164 | assertEquals("9.999e0", ExponentFormatter(9.9992).scientific(7)) 165 | assertEquals("9.999e0", ExponentFormatter(9.9993).scientific(7)) 166 | assertEquals("9.999e0", ExponentFormatter(9.9994).scientific(7)) 167 | assertEquals("1.000e1", ExponentFormatter(9.9995).scientific(7)) 168 | assertEquals("1.000e1", ExponentFormatter(9.9996).scientific(7)) 169 | assertEquals("1.000e1", ExponentFormatter(9.9995).scientific(7)) 170 | assertEquals("1.000e1", ExponentFormatter(9.9998).scientific(7)) 171 | assertEquals("1.000e1", ExponentFormatter(9.9999).scientific(7)) 172 | 173 | assertEquals("3.350e0", x.scientific(7)) 174 | 175 | 176 | assertEquals("3.350e0", "%.3e".sprintf(3.349832285740512 )) 177 | assertEquals("3.350e0", "%.3e".sprintf(3.349832285740512 )) 178 | 179 | assertEquals("1.000e1", "%.3e".sprintf(9.9999 )) 180 | printf("hello, %s", "world") 181 | 182 | assertEquals("1.000e1", "%.3e".sprintf(9.9999 )) 183 | } 184 | 185 | @Test 186 | fun testFractionalFormat() { 187 | assertEquals("3.340", "%.3f".sprintf(3.34011 )) 188 | assertEquals("3.340", "%.3f".sprintf(3.34022 )) 189 | assertEquals("3.340", "%.3f".sprintf(3.34033 )) 190 | assertEquals("3.340", "%.3f".sprintf(3.34044 )) 191 | assertEquals("3.341", "%.3f".sprintf(3.34054 )) 192 | assertEquals("3.341", "%.3f".sprintf(3.34065 )) 193 | assertEquals("3.341", "%.3f".sprintf(3.34076 )) 194 | assertEquals("3.341", "%.3f".sprintf(3.34087 )) 195 | assertEquals("3.341", "%.3f".sprintf(3.34098 )) 196 | 197 | assertEquals("3.399", "%.3f".sprintf(3.39911 )) 198 | assertEquals("3.399", "%.3f".sprintf(3.39921 )) 199 | assertEquals("3.399", "%.3f".sprintf(3.39931 )) 200 | assertEquals("3.399", "%.3f".sprintf(3.39942 )) 201 | 202 | assertEquals("3.400", "%.3f".sprintf(3.39951 )) 203 | assertEquals("3.400", "%.3f".sprintf(3.39961 )) 204 | assertEquals("3.400", "%.3f".sprintf(3.39971 )) 205 | assertEquals("3.400", "%.3f".sprintf(3.39981 )) 206 | assertEquals("3.400", "%.3f".sprintf(3.39991 )) 207 | 208 | assertEquals("10.0", "%.1f".sprintf(9.99991 )) 209 | 210 | // older tests 211 | // assertEquals("3.399", "%.3f".sprintf(3.390 )) 212 | // assertEquals("3.390", "%.3f".sprintf(3.395 )) 213 | // assertEquals("3.340", "%.3f".sprintf(3.3405 )) 214 | // assertEquals("3.350", "%.3f".sprintf(3.349832285740512 )) 215 | // assertEquals("3.350", "%.3f".sprintf(3.349832285740512 )) 216 | // assertEquals("3.350", "%.3f".sprintf(3.349832285740512 )) 217 | 218 | } 219 | 220 | @Test 221 | fun testAutoFloats() { 222 | assertEquals("17.234", "%g".sprintf(17.234)) 223 | assertEquals("**17.234", "%*8g".sprintf(17.234)) 224 | assertEquals("+017.234", "%+08g".sprintf(17.234)) 225 | } 226 | 227 | @Test 228 | fun testStrings() { 229 | assertEquals("== 3 ==","== %s ==".sprintf(3)) 230 | assertEquals("== 3 ==","== %3s ==".sprintf(3)) 231 | assertEquals("== 3 ==","== %-3s ==".sprintf(3)) 232 | assertEquals("== 3 ==","== %^3s ==".sprintf(3)) 233 | assertEquals("== **3** ==","== %*^5s ==".sprintf(3)) 234 | assertEquals("== __3__ ==","== %_^5s ==".sprintf(3)) 235 | assertEquals("*****hello!","%*10s!".sprintf("hello")) 236 | assertEquals("Hello, world!","%s, %s!".sprintf("Hello", "world")) 237 | assertEquals("___centered___","%^_14s".sprintf("centered")) 238 | } 239 | 240 | @Test 241 | fun testCharacters() { 242 | assertEquals("Cat!", "Ca%c!".sprintf('t')) 243 | assertEquals("Cat!", "Ca%C!".sprintf('t')) 244 | } 245 | 246 | @Test 247 | fun testOctals() { 248 | assertEquals("7 10", "%o %o".sprintf(7,8)) 249 | assertEquals("007 010", "%03o %03o".sprintf(7,8)) 250 | } 251 | 252 | @Test 253 | fun testTime() { 254 | // val t = Clock.System.now() 255 | val t = LocalDateTime(1970, 5, 6, 5, 45, 11, 123456789 ) 256 | // println("%tH:%tM:%tS.%tL (%tN)".sprintf(t, t, t, t, t, t)) 257 | assertEquals("05:45:11.123 (123456789)","%1\$tH:%1\$tM:%1\$tS.%1\$tL (%1\$tN)".sprintf(t)) 258 | assertEquals("05:45:11.123 (123456789)","%1!tH:%1!tM:%1!tS.%1!tL (%1!tN)".sprintf(t)) 259 | 260 | assertEquals("May 6, 1970","%1!tB %1!te, %1!tY".sprintf(t)) 261 | assertEquals("06.05.1970","%1!td.%1!tm.%1!tY".sprintf(t)) 262 | assertEquals("06.05.70","%1!td.%1!tm.%1!ty".sprintf(t)) 263 | assertEquals("06.May.70","%1!td.%1!th.%1!ty".sprintf(t)) 264 | assertEquals("06.May.70","%1!td.%1!tB.%1!ty".sprintf(t)) 265 | assertEquals("Day 126, it was Wednesday.","Day %tj, it was %1!tA.".sprintf(t)) 266 | assertEquals("Day 126, it was Wed.","Day %tj, it was %1!ta.".sprintf(t)) 267 | 268 | assertEquals("05:45","%tR".sprintf(t)) 269 | assertEquals("05:45:11","%tT".sprintf(t)) 270 | 271 | val t1 = LocalDateTime(1970, 5, 6, 15, 45, 11, 123456789 ) 272 | assertEquals("05:45:11 AM","%Tr".sprintf(t)) 273 | assertEquals("03:45:11 pm","%tr".sprintf(t1)) 274 | 275 | assertEquals("05/06/70","%tD".sprintf(t)) 276 | assertEquals("1970-05-06","%tF".sprintf(t)) 277 | // no idea how to test it without time zone dependency 278 | // assertEquals("Wed May 06 05:45:11 +01:00 1970","%tc".sprintf(t.toInstant(TimeZone.currentSystemDefault()))) 279 | 280 | assertTrue { "%tO".sprintf(t).startsWith("1970-05-06T05:45:11") } 281 | } 282 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/tools.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.ExperimentalCoroutinesApi 2 | import kotlinx.coroutines.coroutineScope 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.launch 5 | import kotlinx.coroutines.sync.Mutex 6 | import kotlinx.coroutines.test.runTest 7 | import kotlinx.datetime.Clock 8 | import net.sergeych.mp_tools.* 9 | import net.sergeych.mptools.isInFuture 10 | import net.sergeych.mptools.isInPast 11 | import net.sergeych.mptools.withReentrantLock 12 | import kotlin.random.Random 13 | import kotlin.random.nextInt 14 | import kotlin.test.Test 15 | import kotlin.test.assertContentEquals 16 | import kotlin.test.assertEquals 17 | import kotlin.test.assertTrue 18 | import kotlin.time.Duration.Companion.seconds 19 | import kotlin.time.ExperimentalTime 20 | 21 | @ExperimentalTime 22 | class TestTools { 23 | 24 | @Test 25 | fun testBase64() { 26 | var src = byteArrayOf(1,3,4,4) 27 | assertEquals(src.encodeToBase64Compact(), "AQMEBA") 28 | assertEquals(src.encodeToBase64(), "AQMEBA==") 29 | assertContentEquals(src, "AQMEBA".decodeBase64Compact()) 30 | assertContentEquals(src, "AQMEBA==".decodeBase64()) 31 | 32 | for( i in 0..10) { 33 | for (s in 1..117) { 34 | src = Random.Default.nextBytes(s) 35 | var x = src.encodeToBase64() 36 | assertContentEquals(src, x.decodeBase64()) 37 | x = src.encodeToBase64Compact() 38 | assertContentEquals(src, x.decodeBase64Compact()) 39 | } 40 | } 41 | 42 | // this should not fail (multiline, spaces before and after: 43 | """ 44 | 45 | JgAcAQABvID3cUi1Rk8XEdu+BSs2Kodi6kkd41LVM67i2uBwfQw08da+Ve5Vb/XVq095TLHSzugFliL4 46 | u57b4WEiNEDctWHSa441YZe+UO/VHvRkobKo87FZ6yWtp5YgduZ+YtFrAg6QVLEYw5pBUdoY7d84N49I 47 | myORomyn6JylYXUQv/Gob7yA52m9fiCKZaX01kUj6T9fiMDmI9KLbdJVJrfrlGaJOgXd1cQDGfwVmQhs 48 | 1kMrTvZhMy4MNInySAPxfxsEBUM1n702lkO1mUz7s3vxaIjr6iGOInVJ9UXqGBRXTMpsg9+hsOfINAKj 49 | 4OuND88Dwy5R31GMiReAt01Qlg57L1MzY2M= 50 | 51 | 52 | """.decodeBase64() 53 | } 54 | 55 | @Test 56 | fun putInt() { 57 | val N = 256+4 58 | val bd = ByteArray(N) 59 | 60 | fun t(value:Int) { 61 | val index = Random.nextInt(0..N-4) 62 | bd.putInt(index, value) 63 | assertEquals(value, bd.getInt(index)) 64 | } 65 | 66 | for( i in 0..1000) { 67 | val x = Random.nextInt(1, Int.MAX_VALUE) 68 | t(x) 69 | } 70 | } 71 | 72 | @Test 73 | fun searchInByteArray() { 74 | val offset = 1171 75 | val needle = "Fake vaccine kills".encodeToByteArray() 76 | val haystack = Random.nextBytes(offset) + needle + Random.nextBytes(offset/3) 77 | assertEquals(offset, haystack.indexOf(needle)) 78 | } 79 | 80 | @OptIn(ExperimentalCoroutinesApi::class) 81 | @Test 82 | fun reentrantMutex() = runTest { 83 | val m = Mutex() 84 | var x = 0 85 | coroutineScope { 86 | for (i in 1..100) { 87 | launch { 88 | m.withReentrantLock { 89 | val t = x 90 | delay(10) 91 | m.withReentrantLock { x = t + 1 } 92 | } 93 | } 94 | } 95 | } 96 | assertEquals(100, x) 97 | } 98 | 99 | @Test 100 | fun timeTools() { 101 | val x = Clock.System.now() 102 | assertTrue { (x - 1.seconds).isInPast } 103 | assertTrue { (x + 10.seconds).isInFuture } 104 | } 105 | 106 | @Test 107 | fun testTrim() { 108 | assertEquals("12…", "12345".trimToEllipsis(3)) 109 | assertEquals("123", "123".trimToEllipsis(3)) 110 | assertEquals("12", "12".trimToEllipsis(3)) 111 | assertEquals("1", "1".trimToEllipsis(3)) 112 | assertEquals("", "".trimToEllipsis(3)) 113 | 114 | assertEquals("12…9", "123456789".trimMiddle(4)) 115 | assertEquals("12…B", "123456789AB".trimMiddle(4)) 116 | assertEquals("12…AB", "123456789AB".trimMiddle(5)) 117 | assertEquals("123AB", "123AB".trimMiddle(5)) 118 | assertEquals("123", "123".trimMiddle(5)) 119 | assertEquals("", "".trimMiddle(5)) 120 | } 121 | 122 | @Test 123 | fun testThousands() { 124 | assertEquals("1", 1.withThousandsSeparator()) 125 | assertEquals("12", 12.withThousandsSeparator()) 126 | assertEquals("123", 123.withThousandsSeparator()) 127 | assertEquals("-123", (-123).withThousandsSeparator()) 128 | assertEquals("1 234", 1234.withThousandsSeparator()) 129 | assertEquals("12 234", 12234.withThousandsSeparator()) 130 | assertEquals("123 234", 123234.withThousandsSeparator()) 131 | assertEquals("12 123 234", 12123234.withThousandsSeparator()) 132 | assertEquals("1 123 234", 1123234.withThousandsSeparator()) 133 | assertEquals("1 123 234", 1123234.withThousandsSeparator()) 134 | assertEquals("12 123 234", 12123234.withThousandsSeparator()) 135 | assertEquals("123 123 234", 123123234.withThousandsSeparator()) 136 | assertEquals("123 123 234", 123123234.withThousandsSeparator()) 137 | assertEquals("1 123 123 234", 1123123234.withThousandsSeparator()) 138 | assertEquals("-1 123 123 234", (-1123123234).withThousandsSeparator()) 139 | 140 | assertEquals("22.33", 22.33.withThousandsSeparator()) 141 | assertEquals("2.33", 2.33.withThousandsSeparator()) 142 | assertEquals("0.33", 0.33.withThousandsSeparator()) 143 | assertEquals("-0.33", (-0.33).withThousandsSeparator()) 144 | assertEquals("1 234.33", (1_234.33).withThousandsSeparator()) 145 | assertEquals("-1 234.33", (-1_234.33).withThousandsSeparator()) 146 | } 147 | 148 | // @Test 149 | // fun testTime() = runTest { 150 | // val x1 = cur 151 | // } 152 | 153 | } -------------------------------------------------------------------------------- /src/iosArm64Main/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | // no extra setup is needed 5 | } -------------------------------------------------------------------------------- /src/iosArm64Main/kotlin/net/sergeych/sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | // TODO: please add ios-specific conversions here 7 | throw IllegalArgumentException("can't convert to time instant: $t") 8 | } -------------------------------------------------------------------------------- /src/iosArm64Main/kotlin/net/sergeych/sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 4 | // TODO: Get current locale mpnth name somehow 5 | return null 6 | } 7 | 8 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 9 | // TODO extract local month name abbreviations 10 | return null 11 | } 12 | 13 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 14 | return null 15 | } 16 | 17 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 18 | return null 19 | } -------------------------------------------------------------------------------- /src/iosSimulatorArm64Main/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | } -------------------------------------------------------------------------------- /src/iosSimulatorArm64Main/kotlin/net/sergeych/sprintf/Specification.iosSimulatorArm64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 4 | // TODO: Get current locale mpnth name somehow 5 | return null 6 | } 7 | 8 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 9 | // TODO extract local month name abbreviations 10 | return null 11 | } 12 | 13 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 14 | return null 15 | } 16 | 17 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 18 | return null 19 | } -------------------------------------------------------------------------------- /src/iosSimulatorArm64Main/kotlin/net/sergeych/sprintf/Sprintf.iosSimulatorArm64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | throw IllegalArgumentException("can't convert to time instant: $t") 7 | } -------------------------------------------------------------------------------- /src/iosX64Main/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | // no extra setup is needed 5 | } -------------------------------------------------------------------------------- /src/iosX64Main/kotlin/net/sergeych/sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | // TODO: please add ios-specific conversions here 7 | throw IllegalArgumentException("can't convert to time instant: $t") 8 | } -------------------------------------------------------------------------------- /src/iosX64Main/kotlin/net/sergeych/sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 4 | // TODO: Get current locale mpnth name somehow 5 | return null 6 | } 7 | 8 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 9 | // TODO extract local month name abbreviations 10 | return null 11 | } 12 | 13 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 14 | return null 15 | } 16 | 17 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 18 | return null 19 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | // nothing to do 5 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/net/sergeych/sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlin.js.Date 5 | 6 | actual fun ConvertToInstant(t: Any): Instant = when(t) { 7 | is Date -> Instant.fromEpochMilliseconds(t.getTime().toLong()) 8 | else -> throw IllegalArgumentException("Can't convert to LocalDateTime: $t") 9 | } 10 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/net/sergeych/sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | /** 4 | * Platform could provide current locale based month name or return null to use English 5 | */ 6 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 7 | // TODO extract local month names 8 | return null 9 | } 10 | 11 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 12 | // TODO extract local month name abbreviations 13 | return null 14 | } 15 | 16 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 17 | return null 18 | } 19 | 20 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 21 | return null 22 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | import kotlinx.coroutines.runBlocking 4 | 5 | internal actual fun ConsoleLoggerSetup() { 6 | // we do not want our log to loose last mesages on shutdown: 7 | Runtime.getRuntime().addShutdownHook(Thread { 8 | runBlocking { Log.disconnectConsole() } }) 9 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/net/sergeych/mp_logger/FileLogCatcher.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.runBlocking 6 | import kotlinx.coroutines.withContext 7 | import net.sergeych.mp_tools.AsyncBouncer 8 | import net.sergeych.mp_tools.globalLaunch 9 | import net.sergeych.mptools.Now 10 | import net.sergeych.sprintf.sprintf 11 | import java.io.BufferedWriter 12 | import java.io.File 13 | import java.io.FileOutputStream 14 | import java.util.zip.GZIPOutputStream 15 | import java.util.zip.ZipEntry 16 | import java.util.zip.ZipOutputStream 17 | import kotlin.time.Duration.Companion.milliseconds 18 | 19 | class FileLogCatcher(name: String, level: Log.Level = Log.Level.DEBUG, rotate: Boolean = false) { 20 | 21 | private val currentLog: File 22 | 23 | // private var queue = LinkedBlockingQueue() 24 | 25 | /** 26 | * Export current logs to zip, with optional metadata. It exports current 27 | * log as `current.log` and, if exists, `previous.log`. 28 | * 29 | * @param metadata if present, will be put as `metadata.txt` file 30 | * @param output if present, zip wile will be written to it (Existing file will be deleted). 31 | * Otherwise new temp wile in delete-on-exit mode will be created. 32 | * @return ready zip file 33 | */ 34 | @Suppress("unused") 35 | fun exportZip(metadata: String? = null, output: File? = null): File { 36 | val f = if (output != null) { 37 | if (output.exists()) output.delete() 38 | output 39 | } else { 40 | val f = File.createTempFile("exprort", "log") 41 | f.deleteOnExit() 42 | f 43 | } 44 | val zos = ZipOutputStream(f.outputStream()) 45 | zos.setLevel(9) 46 | if (metadata != null) { 47 | zos.putNextEntry(ZipEntry("metadata.txt")) 48 | zos.write(metadata.encodeToByteArray()) 49 | } 50 | zos.putNextEntry(ZipEntry("current.log")) 51 | zos.write(currentLog.readBytes()) 52 | zos.closeEntry() 53 | zos.close() 54 | return f 55 | } 56 | 57 | val job: Job 58 | private val out: BufferedWriter 59 | private val bouncer: AsyncBouncer 60 | 61 | suspend fun close() { 62 | try { 63 | if (!bouncer.isClosed) { 64 | bouncer.close() 65 | job.cancel() 66 | withContext(Dispatchers.IO) { 67 | out.close() 68 | } 69 | } 70 | } catch (x: Throwable) { 71 | x.printStackTrace() 72 | } 73 | } 74 | 75 | suspend fun flush() { 76 | bouncer.pulse(true) 77 | withContext(Dispatchers.IO) { 78 | out.flush() 79 | } 80 | } 81 | 82 | init { 83 | currentLog = File(name) 84 | currentLog.parentFile?.let { root -> 85 | if (!root.exists()) root.mkdirs() 86 | } 87 | 88 | val lf = LogFormatter() 89 | val stream = if (currentLog.exists()) { 90 | if (rotate) { 91 | val rotated = File("%s_%t#.gz".sprintf(name, Now())) 92 | val gz = GZIPOutputStream(rotated.outputStream()) 93 | currentLog.inputStream().use { it.copyTo(gz) } 94 | gz.close() 95 | currentLog.outputStream() 96 | } else { 97 | FileOutputStream(currentLog, true) 98 | } 99 | } else 100 | currentLog.outputStream() 101 | out = stream.bufferedWriter() 102 | 103 | println("writing to $currentLog") 104 | 105 | bouncer = AsyncBouncer(100.milliseconds, 100.milliseconds) { withContext(Dispatchers.IO) { 106 | out.flush() 107 | } } 108 | 109 | job = globalLaunch { 110 | Log.logFlow.collect { 111 | if (it.level >= level) 112 | bouncer.performAndPulse { 113 | for (line in lf.format(it)) { 114 | withContext(Dispatchers.IO) { 115 | out.write(line) 116 | out.newLine() 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | Runtime.getRuntime().addShutdownHook(Thread { 124 | runBlocking { close() } 125 | }) 126 | 127 | } 128 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/net/sergeych/sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | import java.time.ZoneId 5 | import java.time.ZonedDateTime 6 | import java.util.* 7 | 8 | actual fun ConvertToInstant(t: Any): Instant = 9 | when (t) { 10 | is java.time.LocalDateTime -> Instant.fromEpochSeconds( 11 | t.atZone(ZoneId.systemDefault()).toEpochSecond() 12 | ) 13 | is ZonedDateTime -> Instant.fromEpochSeconds(t.toEpochSecond()) 14 | is java.time.Instant -> Instant.fromEpochMilliseconds(t.toEpochMilli()) 15 | is Date -> Instant.fromEpochMilliseconds(t.time) 16 | else -> throw IllegalArgumentException("Can't convert to LocalDateTime: $t") 17 | } 18 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/net/sergeych/sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | /** 4 | * Platform could provide current locale based month name or return null to use English 5 | */ 6 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 7 | return null 8 | } 9 | 10 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 11 | // TODO extract local month name abbreviations 12 | return null 13 | } 14 | 15 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 16 | return null 17 | } 18 | 19 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 20 | return null 21 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/net/sergeych/tools/CachedSyncExpression.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.tools 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.Instant 6 | import net.sergeych.mptools.withReentrantLock 7 | import kotlin.time.Duration 8 | 9 | /** 10 | * Like [CachedExpression] but working in sync mode, no coroutines required, hence only for JVM target. 11 | */ 12 | class CachedSyncExpression( 13 | /** 14 | * If not null, recalculated cached expression is automaticallu invalidated after this time. Set it to null 15 | * to keep it indefinitely until [clearCache] is called 16 | */ 17 | var expiresIn: Duration? = null, 18 | initialValue: T? = null, 19 | ) { 20 | 21 | private var cachedValue: T? = initialValue 22 | private var cacheSetAt: Instant? = initialValue?.let { Clock.System.now() } 23 | private val access = Object() 24 | 25 | /** 26 | * If there is a cached value it will be dropped 27 | */ 28 | fun clearCache() { 29 | synchronized(access) { cachedValue = null } 30 | } 31 | 32 | /** 33 | * The cached value is overriden or set to a specified value. It meands, next [get] call will not 34 | * calculate expression but return the value provided. 35 | */ 36 | @Suppress("unused") 37 | fun overrideCacheWith(value: T) { 38 | synchronized(access) { cachedValue = value; cacheSetAt = Clock.System.now() } 39 | } 40 | 41 | /** 42 | * Return cache value, if presented and not expired. See [expiresIn]. 43 | */ 44 | fun cachedOrNull(): T? = synchronized(access) { 45 | if (cachedValue != null) { 46 | expiresIn?.let { d -> 47 | val setAt = cacheSetAt ?: throw IllegalStateException("cached value is set but cacheSetAt is null") 48 | if (setAt + d < Clock.System.now()) 49 | cachedValue = null 50 | } 51 | } 52 | cachedValue 53 | } 54 | 55 | 56 | /** 57 | * Return cached value if exists and not expired, or recalculates new one and caches it. [producer] will either 58 | * be called _before_ return, or not be called at all, so it is safe to use dusposable resource in it. 59 | * @param producer lambda expresson to calculate actual value, that will be cached for subsequent calls 60 | */ 61 | fun get(producer: () -> T) = synchronized(access) { 62 | cachedOrNull() ?: producer().also { 63 | cacheSetAt = Clock.System.now() 64 | cachedValue = it 65 | } 66 | } 67 | 68 | /** 69 | * Try to get the expression value from the block if it is not already cached. If the block returns 70 | * null, just return it. 71 | */ 72 | fun optGet(producer: () -> T?) = synchronized(access) { 73 | cachedOrNull() ?: producer().also { 74 | if (it != null) { 75 | cacheSetAt = Clock.System.now() 76 | cachedValue = it 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/mo_logger/TestLogger.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalCoroutinesApi::class) 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.runBlocking 6 | import kotlinx.coroutines.test.runTest 7 | import kotlinx.datetime.Clock 8 | import net.sergeych.mp_logger.* 9 | import net.sergeych.mptools.Now 10 | import net.sergeych.sprintf.sprintf 11 | import kotlin.test.Test 12 | 13 | 14 | //class TA: Loggable by LogTag("TA") { 15 | // 16 | //} 17 | // 18 | //class TB: Loggable by LogTag("TB") { 19 | // 20 | //} 21 | 22 | class TestLogger { 23 | // this code will not work on native (because of theri STUPID atomic-fu and all other shot0fu and 24 | // single-threadness-fu of underqualified system architects. Now looking for a way to limit tests to non-native 25 | // implementations 26 | @Test 27 | fun testConsole() = runBlocking { 28 | val x = object : Loggable by LogTag("TSTOB") {} 29 | try { 30 | x.info { "that should not be missing (replay)" } 31 | Log.connectConsole() 32 | x.info { "this one should be shown" } 33 | println("--- pre delay --- ${Clock.System.now()}") 34 | delay(30) 35 | println("--- post delay ---${Clock.System.now()}") 36 | } 37 | finally { 38 | Log.disconnectConsole() 39 | } 40 | } 41 | 42 | @Test 43 | fun testShutdownHook() = runTest { 44 | Log.connectConsole(Log.Level.DEBUG) 45 | val x = LogTag("test") 46 | x.debug { "Debug" } 47 | x.info { "Info" } 48 | } 49 | 50 | @Test 51 | fun fileTest() { 52 | runBlocking { 53 | Log.connectConsole(Log.Level.DEBUG) 54 | println("%t#".sprintf(Now())) 55 | // val b = AsyncBouncer(100.milliseconds) { 56 | // println("test bouncer") 57 | // } 58 | // b.pulse(true) 59 | val c = FileLogCatcher("testlog",rotate = true) 60 | val x = LogTag("TFILE") 61 | delay(300) 62 | for(i in 1..20) { 63 | x.info { "--------- we run ------------" } 64 | delay(50) 65 | } 66 | delay(4000) 67 | 68 | } 69 | } 70 | 71 | // @Test 72 | // fun levels() { 73 | // runBlocking { 74 | // Log.connectConsole() 75 | // 76 | // val ta = TA() 77 | // val tb = TB() 78 | // 79 | // fun d(text: String) { 80 | // ta.debug { text }; tb.debug { text } 81 | // } 82 | // 83 | // fun i(text: String) { 84 | // ta.info { text }; tb.info { text } 85 | // } 86 | // 87 | // d("m1") 88 | // ta.logLevel = Log.Level.DEBUG 89 | // d("m2") 90 | // d("m3") 91 | // i("m4") 92 | // 93 | //// val list = Log.log. 94 | // delay(1200) 95 | //// println("-- log -- ${list.size}") 96 | //// for (l in list) println("--> $l") 97 | //// assertEquals(3, list.size) 98 | // } 99 | // } 100 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/mo_logger/testtoolsJVM.kt: -------------------------------------------------------------------------------- 1 | package mo_logger 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.runBlocking 5 | import net.sergeych.mptools.CachedExpression 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNull 9 | import kotlin.time.Duration.Companion.milliseconds 10 | 11 | class ToolsTestJVM { 12 | @Test 13 | fun cachedExpression() = runBlocking { 14 | val ce = CachedExpression(20.milliseconds) 15 | assertNull(ce.cachedOrNull()) 16 | assertEquals("foo", ce.get { "foo" }) 17 | assertEquals("foo", ce.get { "bar" }) 18 | delay(70) 19 | assertEquals("bar", ce.get { "bar" }) 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/net/sergeych/jvm_tools.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych 2 | 3 | import net.sergeych.mp_tools.getInt 4 | import java.nio.ByteBuffer 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class TestBinaryInteroperability { 9 | @Test 10 | fun getInt() { 11 | val N = 256+4 12 | val bb = ByteBuffer.allocate(N) 13 | val bd = bb.array() 14 | for( i in 0 until N) { 15 | bb.array()[i] = i.toByte() 16 | } 17 | for( i in 0 ..N-4) { 18 | assertEquals(bb.getInt(i), bd.getInt(i)) 19 | } 20 | 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/macosArm64Main/kotlin/net/sergeych/mp_logger/Loggable.macosArm64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | } -------------------------------------------------------------------------------- /src/macosArm64Main/kotlin/net/sergeych/sprintf/Specification.macosArm64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 4 | // TODO: Get current locale mpnth name somehow 5 | return null 6 | } 7 | 8 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 9 | // TODO extract local month name abbreviations 10 | return null 11 | } 12 | 13 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 14 | return null 15 | } 16 | 17 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 18 | return null 19 | } -------------------------------------------------------------------------------- /src/macosArm64Main/kotlin/net/sergeych/sprintf/Sprintf.macosArm64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | throw IllegalArgumentException("can't convert to time instant: $t") 7 | } -------------------------------------------------------------------------------- /src/macosX64Main/kotlin/net/sergeych/mp_logger/Loggable.macosX64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | } -------------------------------------------------------------------------------- /src/macosX64Main/kotlin/net/sergeych/sprintf/Specification.macosX64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 4 | // TODO: Get current locale mpnth name somehow 5 | return null 6 | } 7 | 8 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 9 | // TODO extract local month name abbreviations 10 | return null 11 | } 12 | 13 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 14 | return null 15 | } 16 | 17 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 18 | return null 19 | } -------------------------------------------------------------------------------- /src/macosX64Main/kotlin/net/sergeych/sprintf/Sprintf.macosX64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | // kotlin native date types... have no idea what these are 7 | // pls add your code here and create PR from it 8 | throw IllegalArgumentException("can't convert to time instant: $t") 9 | } -------------------------------------------------------------------------------- /src/mingwX64Main/kotlin/net.sergeych.sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | // kotlin native date types... have no idea what these are 7 | // pls add your code here and create PR from it 8 | throw IllegalArgumentException("can't convert to time instant: $t") 9 | } -------------------------------------------------------------------------------- /src/mingwX64Main/kotlin/net.sergeych.sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | /** 4 | * Platform could provide current locale based month name or return null to use English 5 | */ 6 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 7 | // TODO: Get current locale mpnth name somehow 8 | return null 9 | } 10 | 11 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 12 | // TODO extract local month name abbreviations 13 | return null 14 | } 15 | 16 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 17 | return null 18 | } 19 | 20 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 21 | return null 22 | } -------------------------------------------------------------------------------- /src/mingwX64Main/kotlin/net/sergeych/mp_logger/Loggable.mingwX64.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | } -------------------------------------------------------------------------------- /src/nativeMain/kotlin/net/sergeych/mp_logger/ConsoleLoggerSetup.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | // nothing to do 5 | } -------------------------------------------------------------------------------- /src/nativeMain/kotlin/net/sergeych/mp_logger/toConsole.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | -------------------------------------------------------------------------------- /src/nativeMain/kotlin/net/sergeych/sprintf/ConvertToInstant.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | actual fun ConvertToInstant(t: Any): Instant { 6 | // kotlin native date types... have no idea what these are 7 | // pls add your code here and create PR from it 8 | throw IllegalArgumentException("can't convert to time instant: $t") 9 | } -------------------------------------------------------------------------------- /src/nativeMain/kotlin/net/sergeych/sprintf/LocaleSpecificMonthName.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | /** 4 | * Platform could provide current locale based month name or return null to use English 5 | */ 6 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 7 | // TODO: Get current locale mpnth name somehow 8 | return null 9 | } 10 | 11 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 12 | // TODO extract local month name abbreviations 13 | return null 14 | } 15 | 16 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 17 | return null 18 | } 19 | 20 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 21 | return null 22 | } -------------------------------------------------------------------------------- /src/wasmJsMain/kotlin/net/sergeych/mp_logger/Loggable.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.mp_logger 2 | 3 | internal actual fun ConsoleLoggerSetup() { 4 | // nothing to do 5 | } -------------------------------------------------------------------------------- /src/wasmJsMain/kotlin/net/sergeych/sprintf/Specification.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | /** 4 | * Platform could provide current locale based month name or return null to use English. Month number is 1..12 5 | * as default in date operations in java 6 | */ 7 | actual fun LocaleSpecificMonthName(monthNumber: Int): String? { 8 | return null 9 | } 10 | 11 | actual fun LocaleSpecificAbbreviatedMonthName(monthNumber: Int): String? { 12 | return null 13 | } 14 | 15 | actual fun LocaleSpecificDayName(isoDayNumber: Int): String? { 16 | return null 17 | } 18 | 19 | actual fun LocaleSpecificAbbreviatedDayName(isoDayNumber: Int): String? { 20 | return null 21 | } -------------------------------------------------------------------------------- /src/wasmJsMain/kotlin/net/sergeych/sprintf/Sprintf.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package net.sergeych.sprintf 2 | 3 | import kotlinx.datetime.Instant 4 | actual fun ConvertToInstant(t: Any): Instant { 5 | throw IllegalArgumentException("Can't convert to LocalDateTime: $t") 6 | } 7 | --------------------------------------------------------------------------------