{"id":32957737,"url":"https://github.com/aptusproject/aptus-core","last_synced_at":"2026-01-14T02:36:37.095Z","repository":{"id":40584559,"uuid":"370787558","full_name":"aptusproject/aptus-core","owner":"aptusproject","description":"A utility library aiming to simplify the Scala coding experience.","archived":false,"fork":false,"pushed_at":"2025-06-19T14:05:12.000Z","size":1984,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-11-16T18:01:32.659Z","etag":null,"topics":["scala","utilities"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aptusproject.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-05-25T18:13:35.000Z","updated_at":"2025-06-19T14:05:23.000Z","dependencies_parsed_at":"2024-02-27T19:30:26.093Z","dependency_job_id":"c47bdde4-5387-4dbf-8bc3-0d6ea29a7f11","html_url":"https://github.com/aptusproject/aptus-core","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/aptusproject/aptus-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aptusproject%2Faptus-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aptusproject%2Faptus-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aptusproject%2Faptus-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aptusproject%2Faptus-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aptusproject","download_url":"https://codeload.github.com/aptusproject/aptus-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aptusproject%2Faptus-core/sbom","scorecard":{"id":204476,"data":{"date":"2025-08-11","repo":{"name":"github.com/aptusproject/aptus-core","commit":"ce84315da4e8fb8d8a3c2ff3bf206068a2d2dc1b"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-16T23:24:28.515Z","repository_id":40584559,"created_at":"2025-08-16T23:24:28.515Z","updated_at":"2025-08-16T23:24:28.515Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["scala","utilities"],"created_at":"2025-11-12T23:00:31.033Z","updated_at":"2026-01-14T02:36:37.080Z","avatar_url":"https://github.com/aptusproject.png","language":"Scala","readme":"\u003cp align=\"center\"\u003e\u003cimg src=\"./images/logo.png\" alt=\"icon\" style=\"height: 25%; width: 25%;\"\u003e\u003c/p\u003e\n\n\u003c!-- =========================================================================== --\u003e\n# Aptus\n\n\"Aptus\" is latin for suitable, appropriate, fitting. It is a utility library meant to improve the Scala experience for simple tasks,\nwhen performance isn't most important. It also helps you code defensively when representing errors in types isn't important (think `assert`).\n\n\u003c!-- =========================================================================== --\u003e\n# Introduction\nFor a good introduction to the library, see my talk from the Functional Scala 2024 conference: [video on YouTube](https://youtu.be/Bkm2GPyhf0Y?si=_ROBq0hEH8-fR4jl\u0026t=989)\n\nIn particular the talk discusses the next exciting development for Aptus: bringing quick and simple dynamic data manipulations to the library, for instance:\n\n```scala\n import aptus.dyn._\n\n \"/path/to/my.tsv\" // eg: name,age,occupation,pets\n  .dyns\n    .rename   (\"occupation\" ~\u003e \"job\")\n    .increment(\"age\")\n    .remove   (\"pets\")\n  .write(\"/path/to/my.jsonl\") // one JSON doc per line\n```\n\nNot mentioned in the talk: the ability to go to/from case classes - it wasn't implemented  back then; __coming soon__: ability to interact with Python via _ScalaPy_ (think pandas)\n\n\u003c!-- =========================================================================== --\u003e\n## SBT\n\u003ca name=\"211006113932\"\u003e\u003c/a\u003e\n`libraryDependencies += \"io.github.aptusproject\" %% \"aptus-core\" % \"0.7.0\"`\n\nThen import the following to test it out:\n\n```scala\nimport aptus.all._\n```\n\nThough in general a more piecemeal approach is recommended:\n```scala\nimport aptus.min._\n  OR\npackage object someprojectpackage extends aptus.Minimal\n```\n\nAlongside some ad hoc imports where needed:\n```scala\nimport aptus.Map_\nimport aptus.OutputFilePath\n...\n```\n\nThe library is available for Scala\n[3.4.0](https://search.maven.org/artifact/io.github.aptusproject/aptus-core_3/0.7.0/jar) and\n[2.13](https://search.maven.org/artifact/io.github.aptusproject/aptus-core_2.13/0.7.0/jar)\n\n\n\u003c!-- =========================================================================== --\u003e\n## Dependency graph\n\u003ca name=\"210121153149\"\u003e\u003c/a\u003e\n\n\u003cdiv style=\"text-align:center\"\u003e\u003cimg src=\"./dependencies.png\" alt=\"core dependency graph\"\u003e\u003c/div\u003e\n\nNote: _gson_ will soon be replaced with _ujson_\n\n\u003c!-- =========================================================================== --\u003e\n## Motivation\n\u003ca name=\"210531095628\"\u003e\u003c/a\u003e\n\nI created Aptus in bits over the past 10 years, as I struggled to get seemingly simple tasks done in Scala. It is not intended to be comprehensive, or particularly optimized.\nIt should be seen more as a starting point for a project, where performance isn't most critical and compute resources aren't too limited.\nIt can also serve as a reference, from which the basic use of underlying abstractions can be expanded upon as needed.\n\u003ca name=\"240306115815\"\u003e\u003c/a\u003e\nIt's also for people who enjoy Scala's type system and think types shouldn't be thrown out the window (_hissing snake sound_), yet don't feel the need to capture every possible error as types.\nConsider for instance Li Haoyi's post [\"Scala at Scale at Databricks\"](https://www.databricks.com/blog/2021/12/03/scala-at-scale-at-databricks.html), notably this passage:\n\n\u003e Zero usage of \"archetypical\" Scala frameworks: Play, Akka, Scalaz, Cats, ZIO, etc.\n\nThis resonates well with aptus' goals. I like using some of the tools he mentions, but I also want to make sure I have simpler solutions at hand too.\n\n\u003ca name=\"240306115820\"\u003e\u003c/a\u003e\nI included all the dependencies shown in the diagram above because I find that they are required for most non-trivial projects.\nFor instance, what application nowadays does not need to handle JSON at some point?\nOr parse a CSV file? Or handle a bz2 file?\n\n\u003ca name=\"240306115826\"\u003e\u003c/a\u003e\nNote that Aptus is heavily used in my data transformation library: [Gallia](https://github.com/galliaproject/gallia-core), as well as most of my other projects (public and private).\n\n### Defensive coding\n\nLet's consider stdlib's Seq's `.zip` and `.toMap` method for instance. Both will silently discard elements in some situations, and this behavior __will almost never be the desired/expected one__\n(if nothing because it may not be obvious to another maintainer).\n`.zip` for instance will truncate the longer sequence if they are not the same size.\n`.toMap` will discard entries with duplicate keys, keeping only the last one.\nIn almost all real life situations I encountered personnally and where either situation happened, it was the result of an upstream problem: I either meant for the two collections to be the same size for `.zip`,\nand I thought I wouldn't have duplicate keys when using `.toMap`.\nAs a result I create two corresponding methods in aptus, `.zipSameSize` and `.force.map`, which throw a requirement runtime error when either situation occurs.\nI have been using them exclusively for years now, and it has more than paid off in catching errors early.\n\nWe'll see another example of defensive coding in the next section about succinctness: Java's `.split` and `StringOps.split` can also discard elements silently.\n\n### Succinctness\nA good example of succinctness is a method like `splitByWholeSeparatorPreserveAllTokens` from Apache Commons's `StringUtils`,\nand whose semantics feel [more intuitive](https://github.com/aptusproject/aptus-core/blob/d548ae4/src/test/scala/aptustesting/StringTests.scala#L12-L20) to me than those of Java's `String.split`.\nMeanwhile using:\n\n```scala\n\"foo|bar\".splitBy(\"|\")\n```\n\nis a lot more convenient than using:\n\n```scala\nimport org.apache.commons.lang3.StringUtils\nval str = \"foo|bar\"\nif (str.isEmpty()) List(str)\nelse               StringUtils.splitByWholeSeparatorPreserveAllTokens(str, \"|\").toList\n```\n\nIt should be noted that both Java's `String.split` and the stdlib's `StringOps.split` have the very unintuitive behavior of not reporting trailing elements when empty, for instance:\n```scala\nprintln(\"1,2,3,,\".split(',').toList) // List(1, 2, 3)\n```\n\nI try to illustrate such differences in succinctness/consistency/defensiveness of behavior throughout the examples below.\n\n### Practicality\nAnother aspect of Aptus is practicality, for instance I often find myself using expressions such as:\n\n```scala\n\"foo=3\"\n  .splitBy(\"=\")\n  .force.tuple2\n  .mapSecond(_.toInt)\n```\n\nThe stdlib's counterpart would look something like:\n\n```scala\n\"foo=3\"\n  .split('=')\n   match { case Array(x, y: String) =\u003e\n     (x, y.toInt) }\n```\n\nWhich I argue is harder to read/write and less obvious to understand (albeit not a lot more verbose).\n\n\u003c!-- =========================================================================== --\u003e\n## Examples\n\u003ca name=\"211006113933\"\u003e\u003c/a\u003e\n\n#### In-line assertions\n\u003ca name=\"211006113934\"\u003e\u003c/a\u003e\n\nNote: .ensuring from the stdlib does not offer a way to manipulate the value in the error message\n\n```scala\n\"hello\".ensuring(_.size \u003c= 5)                   .toUpperCase.p // prints \"HELLO\" - stdlib\n\n\"hello\".assert (_.size \u003c= 5)                    .toUpperCase.p // prints \"HELLO\"\n\"hello\".assert (_.size \u003c= 5, x =\u003e s\"value=${x}\").toUpperCase.p // prints \"HELLO\" - can't do that with `ensuring()`\n\"hello\".require(_.size \u003c= 5)                    .toUpperCase.p // prints \"HELLO\"\n\"hello\".require(_.size \u003c= 5, x =\u003e s\"value=${x}\").toUpperCase.p // prints \"HELLO\"\n\n// these throw AssertionError\n\"hello\".assert (_.size \u003e  5)                    .toUpperCase.p \n\"hello\".assert (_.size \u003e  5, x =\u003e s\"value=${x}\").toUpperCase.p // \"assertion failed: value=hello\"\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nConvenient for chaining, consider the pure stdlib alternative:\n\u003ca name=\"211005143255\"\u003e\u003c/a\u003e\n\n```scala\n{\n  import util.chaining._\n  \n  \"hello\"\n    .ensuring(_.startsWith(\"h\"))\n    .toUpperCase\n    .pipe(println)\n}\n```\n\n\u003c!-- =========================================================================== --\u003e\n#### In-line printing\n\u003ca name=\"210531093421\"\u003e\u003c/a\u003e\u003ca name=\"printing\"\u003e\u003c/a\u003e\n\nE.g. for quick debugging:\n```scala\n\"hello\".prt               // prints: \"hello\"\n\"hello\".p                 // prints: \"hello\"\n\"hello\".p.toUpperCase.p   // prints: \"hello\", then \"HELLO\"\n\n\"hello\".inspect(_.size).p // prints: \"5\", then \"hello\"\n\"hello\".i      (_.size).p // prints: \"5\", then \"hello\"\n\n1.toString.p // prints \"1\"\n1.str     .p // prints \"1\"\n\n\"hello\".p__          // prints   \"hello\"   and exits program (code 0)\n\"hello\".i__(_.quote) // prints \"\\\"hello\\\"\" and exits program (code 0)\n```\n\n\n\u003c!-- =========================================================================== --\u003e\n#### String operations\n\u003ca name=\"211006113935\"\u003e\u003c/a\u003e\n\n```scala\n\"hello\". append(\" you!\")  .p // prints \"hello you!\"\n\"hello\".prepend(\"well, \") .p // prints \"well, hello\"\n\n\"hello\". appendedAll(\" you!\")  .p // prints \"hello you!\"  - stdlib\n\"hello\".prependedAll(\"well, \") .p // prints \"well, hello\" - stdlib\n\n\"hello\".colon             .p // prints \"hello:\"\n\"hello\".tab               .p // prints \"hello\u003cTAB\u003e\"\n\"hello\".newline           .p // prints \"hello\u003cnew-line\u003e\"\n\n\"hello\".colon  (\"human\")  .p // prints \"hello:human\"\n\"hello\".tab    (\"human\")  .p // prints \"hello\u003cTAB\u003ehuman\"\n\"hello\".newline(\"human\")  .p // prints \"hello\u003cnew-line\u003ehuman\"\n\n\"hello\".quote             .p // prints \"\\\"hello\\\"\"\n\n\"hello|world\"  .splitBy(\"|\").p // prints Seq(hello, world)\n\"hello|world||\".splitBy(\"|\").p // prints Seq(hello, world, , ) - won't unexpectely ignore empty trailing elements\n\n\"a\\tb\\tc\".splitXsv('\\t') // uses commons-csv under the hood to properly handle the split (eg escaping, ...)\n\n\"hello\".padLeft (8, ' ').p // \"   hello\"\n\"hello\".padRight(8, ' ').p // \"hello   \"\n1.str  .padLeft (3, '0').p // \"001\"\n1.str  .padRight(3, '0').p // \"100\"\n\n\"mykey\".   contains(\"my\").p // stdlib\n\"mykey\".notContains(\"MY\").p // negative counterpart\n\n// .. many more, see String_, for instance:\n// - strip{Prefix,Suffix}{Guaranteed,IfApplicable}\n// - remove{Guaranteed,IfApplicable}\n// - toBase64\n// ...\n```\n\nNote: see corresponding [tests](https://github.com/aptusproject/aptus-core/blob/d548ae4/src/test/scala/aptustesting/StringTests.scala#L12-L20)\n\n\u003c!-- =========================================================================== --\u003e\n#### Number operations\n\u003ca name=\"211006113916\"\u003e\u003c/a\u003e\n\n```scala\n3.1416.add       (1).p // 4.1416\n3.1416.multiplyBy(2).p // 6.2832\n...\n3.1416.isInBetween(fromInclusive = 3.0, toExclusive: 4.0).p // true\n// likewise for Int and Long\n```\n\nFor `Double`:\n```\n3.1416     .maxDecimals   (2).p // 3.14 - still a Double (unlike formats below)\n\n3.1416     .formatDecimals(2).p // 3.14\n3.1416.exp .formatDecimals(4).p // 23.1409\n3.1416.log2.formatDecimals(4).p // 1.6515\n```\n\nPersonally, I always have to look up printf's \"% notation\" before using it, so a method like `formatDecimals` make things a lot easier.\n\nAptus also helps with collections of numbers:\n```scala\nSeq(3, 2, 1).mean  .p // 2.0\nSeq(3, 2, 1).minMax.p // (1, 3)\n// ... more: median, stdev, range, IQR, ... (see aptus.Seq_)\n```\n\n\u003c!-- =========================================================================== --\u003e\n#### Time operations\n\u003ca name=\"211006113906\"\u003e\u003c/a\u003e\n\n```scala\n\"2023-06-05\".parseLocalDate.getYear.p // 2023\n\n// also available:\n//   parseLocalDateTime, parseLocalTime, parseInstant, parseOffsetDateTime and parseZonedDateTime\n// and\n//   parseLocalDateTime(pattern), ...\n```\n\n\u003c!-- =========================================================================== --\u003e\n#### Conditional piping (a.k.a conditional \"thrush\")\n\u003ca name=\"210531093424\"\u003e\u003c/a\u003e\u003ca name=\"conditional-piping\"\u003e\u003c/a\u003e\n\n```scala\n\"hello\"  .pipeIf(_.size \u003c= 5)(_.toUpperCase).p // prints \"HELLO\"\n\"bonjour\".pipeIf(_.size \u003c= 5)(_.toUpperCase).p // prints unchanged\n\n3.pipeIf(_ % 2 == 0)(_ + 1).p // prints 3 (unchanged)\n4.pipeIf(_ % 2 == 0)(_ + 1).p // prints 5\n\nval suffixOpt = Some(\"?\")\n\"hello\".pipeOpt(suffixOpt)(suffix =\u003e _ + suffix).p // prints \"hello?\"\n\"hello\".pipeOpt(None)     (suffix =\u003e _ + suffix).p // prints unchanged\n```\n\nSee [discussion](https://users.scala-lang.org/t/implicit-class-for-any-and-or-generic-type/501) on _Scala Users_.\n\nThere also is also a `mapIf` counterpart:\n\n```scala\nSeq(1, 2, 3).mapIf(true) (_ + 1).p // List(2, 3, 4)\nSeq(1, 2, 3).mapIf(_ \u003c 2)(_ + 1).p // List(2, 2, 3)\n```\n\n\u003c!-- =========================================================================== --\u003e\n#### In-line \"to Option\"\n\u003ca name=\"210531093425\"\u003e\u003c/a\u003e\u003ca name=\"in-line-to-option\"\u003e\u003c/a\u003e\n\n```scala\n\"hello\"  .in.someIf(_.size \u003c= 5).p // prints Some(\"hello\")\n\"bonjour\".in.someIf(_.size \u003c= 5).p // prints None\n\n\"hello\"  .in.noneIf(_.size \u003c= 5).p // prints None\n\"bonjour\".in.noneIf(_.size \u003c= 5).p // prints Some(\"bonjour\")\n\n// note: can also use shorthands: inNoneIf/inSomeIf\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\n\u003ca name=\"211006141307\"\u003e\u003c/a\u003e\nConvenient for chaining, consider the pure stdlib alternative:\n```scala\n{\n  val str = \"hello\"\n  val opt = if (str.size \u003c= 5) Some(str) else None\n  println(opt)\n}\n```\n\nNotes:\n- `Option.when` could also be used, but the test part isn't a predicate on the element (which would be much better).\n- Someone on the scala user list also pointed out this alternative: `Some(\"hello\").filter(_.size \u003c= 5)`. While clever, I'd argue the semantics are much less obvious than `\"hello\".in.someIf(_.size \u003c= 5)`.\n\n\u003c!-- =========================================================================== --\u003e\n#### \"force\" disambiguator (Option/Map)\n\u003ca name=\"210531093426\"\u003e\u003c/a\u003e\u003ca name=\"force-disambiguator\"\u003e\u003c/a\u003e\n\n`.get` is polysemic in the standard library, sometimes \"attempting\" to get the result as with `Map` (returns `Option[T]`), sometimes \"forcing\" it as with `Option` (returns `T`)\n   \naptus' `.force` conveys semantics unambiguously:\n\n```scala\nval myOpt = Some(\"foo\")\nval myMap = Map(\"bar\" -\u003e \"foo\")\n\nmyOpt.force       .p // prints \"foo\"\nmyMap.force(\"bar\").p // prints \"foo\"\n\n// versus stdlib way:\nmyOpt.get       .p // prints      \"foo\"  -\u003e forcing \nmyMap.get(\"bar\").p // prints Some(\"foo\") -\u003e attempting\n```\n\n\n\u003c!-- =========================================================================== --\u003e\n### More forcing\n\u003ca name=\"210610085515\"\u003e\u003c/a\u003e\u003ca name=\"forcing\"\u003e\u003c/a\u003e\u003ca name=\"force-tuples\"\u003e\u003c/a\u003e\n\n```scala\nSeq(1)      .force.one     .p // 1\nSeq(1)      .force.option  .p // Some(1)\nSeq( )      .force.option  .p // None\nSeq(1, 2, 3).force.distinct.p // Seq(1, 2, 3)\nSeq(1, 2, 3).force.set     .p // Set(1, 2, 3)\n\nval (first, second)        = Seq(\"foo\", \"bar\")       .force.tuple2\nval (first, second, third) = Seq(\"foo\", \"bar\", \"baz\").force.tuple3\n// ... and so on up to 10\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nBut:\n\u003ca name=\"211006113936\"\u003e\u003c/a\u003e\n\n```scala\nSeq(1, 2)   .force.one      // runtime error\nSeq(1, 2)   .force.option   // runtime error\nSeq(1, 2, 1).force.distinct // runtime error\nSeq(1, 2, 1).force.set      // runtime error\nSeq(1, 2, 3).force.tuple2   // runtime error\n... and so on\n```\n\nThe `.force.one` mechanism is one of the most useful operations, and a much safer bet than simply doing `.head`.\n\n\u003c!-- =========================================================================== --\u003e\n### Help with Options\n\u003ca name=\"211006113937\"\u003e\u003c/a\u003e\n\nTo optional:\n\u003ca name=\"211005135653\"\u003e\u003c/a\u003e\u003ca name=\"to-optional\"\u003e\u003c/a\u003e\n```scala\n   (None   , Some(2))         .toOptionalTuple.p // None\n   (Some(1), None   )         .toOptionalTuple.p // None   \n   (Some(1), Some(2))         .toOptionalTuple.p // Some((1, 2))\n\nSeq(None,    None,    None)   .toOptionalSeq  .p // None\nSeq(Some(1), Some(2), None)   .toOptionalSeq  .p // None\nSeq(Some(1), Some(2), Some(3)).toOptionalSeq  .p // Some(Seq(1, 2, 3))\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nSwapping:\n\u003ca name=\"211005135654\"\u003e\u003c/a\u003e\u003ca name=\"option-swapping\"\u003e\u003c/a\u003e\n\n```scala\n// parameter for .swap is by-name\nSome(\"foo\").swap(\"bar\").p // None\nNone       .swap(\"bar\").p // Some(\"bar\")\n```\n\n\n\u003c!-- =========================================================================== --\u003e\n### Help with Sequences\n\n\u003ca name=\"211005132123\"\u003e\u003c/a\u003e\u003ca name=\"formatting-sequences\"\u003e\u003c/a\u003e\nQuick sequence formatting:\n```scala\nSeq(1, 2, 3). @@.p //    [1, 2, 3]\nSeq(1, 2, 3).#@@.p // #3:[1, 2, 3]\n\nSeq(1, 2, 3).joinln   // one per line\nSeq(1, 2, 3).joinlnln // one per line every other line\n\nSeq(1, 2, 3).joinln.sectionAllOff(\"data:\") // or equivalently below\nSeq(1, 2, 3).section             (\"data:\") // returns:\n/*\n  data:\n      1\n      2\n      3\n*/\n```\n\nAptus also provides help with sorting for common cases, for instance:\n\n```scala\nSeq(\n    Seq(\"d\", \"e\", \"f\"),\n    Seq(\"g\", \"h\", \"i\"),\n    Seq(\"a\", \"b\", \"c\"))\n  .sorted(aptus.seqOrdering[String])\n/*\nreturns:\n\nSeq(\n    Seq(\"a\", \"b\", \"c\"),\n    Seq(\"d\", \"e\", \"f\"),\n    Seq(\"g\", \"h\", \"i\") )))\n*/\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\n### Zip operations\n\u003ca name=\"211005132124\"\u003e\u003c/a\u003e\u003ca name=\"zip-same-size\"\u003e\u003ca name=\"zip\"\u003e\u003c/a\u003e\nMost of the time, we want to zip collections of same size, and we want to code it defensively:\n```scala\nSeq(1, 2, 3).zipSameSize(Seq(4, 5, 6)).p // Seq((1,4), (2,5), (3,6))\nSeq(1, 2, 3).zipSameSize(Seq(4, 5))   .p // runtime error\n```\n\nAsk yourselves: what are legitimate use cases where we zip two collections of different size and are perfectly happy to have the longuest silently truncated?\n\nOther useful `zip`-related operations are:\n\n```scala\nSeq(\"a\", \"b\", \"c\").zipWithIsFirst.map { case (x, first /* for \"a\" here */) =\u003e if (first) ... else ... }\nSeq(\"a\", \"b\", \"c\").zipWithIsLast .map { case (x, last  /* for \"c\" here */) =\u003e if (last)  ... else ... }\n\nSeq(\"a\", \"b\", \"c\").zipWithIndex.p // List((a,0), (b,1), (c,2))\nSeq(\"a\", \"b\", \"c\").zipWithRank .p // List((a,1), (b,2), (c,3))\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nSplitting at head/last:\n\u003ca name=\"211005135654\"\u003e\u003c/a\u003e\u003ca name=\"split-at-head-last\"\u003e\u003c/a\u003e\n\n```scala\nSeq(1, 2, 3).splitAtHead.p // (1,Seq(2, 3))\nSeq(1, 2, 3).splitAtLast.p // (Seq(1, 2),3)\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nContained:\n\u003ca name=\"211005132125\"\u003e\u003c/a\u003e\u003ca name=\"contained\"\u003e\u003c/a\u003e\n```scala\n1.   containedIn(Seq(1, 2, 3)).p // true\n1.notContainedIn(Seq(1, 2, 3)).p // false \n// also available for Set\n```\n\n_Note: Why not use \"contains\" from the stdlib instead? Consider the following situation:_\n```scala\nval ref = Seq(\"2\", \"4\", \"6\")\nSeq(1, 2, 3).map(ref.contains(_.toString))      // cannot do that\nSeq(1, 2, 3).map(x =\u003e ref.contains(x.toString)) // we need an intermediate\nSeq(1, 2, 3).map(_.toString.containedIn(ref))   // unless using containedIn\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\n\u003ca name=\"211005132126\"\u003e\u003c/a\u003e\u003ca name=\"seq-ordering\"\u003e\u003c/a\u003e\nOrdering sequences of sequences (size prevails):\n```scala\nimplicit val ord: Ordering[Seq[Int]] = aptus.seqOrdering\nSeq(Seq(4, 5, 6), Seq(1, 2, 3)).sorted.p // Seq(Seq(1, 2, 3), Seq(4, 5, 6))\nSeq(Seq(4, 5, 6), Seq(1, 2   )).sorted.p // Seq(Seq(1, 2)   , Seq(4, 5, 6))\nSeq(Seq(4, 5)   , Seq(1, 2, 3)).sorted.p // Seq(Seq(4, 5)   , Seq(1, 2, 3))\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nNote: `List` vs `Seq`, see [discussion](https://users.scala-lang.org/t/seq-vs-list-which-should-i-choose/5412) on _Scala Users_.\n\n\n\u003c!-- =========================================================================== --\u003e\n### Help with Maps\n\u003ca name=\"211006113938\"\u003e\u003c/a\u003e\n\nMost of the time, we do not want duplicates to be silently discarded: \n```scala\n// is this what we wanted?\nSeq(1 -\u003e \"a\", 2 -\u003e \"b\", 2 -\u003e \"c\").toMap    .p // Map(1 -\u003e \"a\", 2 -\u003e \"c\")\n\n// likely not\nSeq(1 -\u003e \"a\", 2 -\u003e \"b\", 2 -\u003e \"c\").force.map.p // runtime error\nSeq(1 -\u003e \"a\", 2 -\u003e \"b\")          .force.map.p // Map(1 -\u003e \"a\", 2 -\u003e \"b\")\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nAssociate left/right:\n\u003ca name=\"211005135616\"\u003e\u003c/a\u003e\u003ca name=\"associate-side\"\u003e\u003c/a\u003e\n\n```scala\nSeq(\"foo\", \"bar\")                                    .force.mapLeft(_.toUpperCase).p\nSeq(\"foo\", \"bar\").map(_.associateLeft(_.toUpperCase)).force.map.p\n  // returns: Map(\"FOO\" -\u003e \"foo\", \"BAR\" -\u003e \"bar\")\n\nSeq(\"foo\", \"bar\")                                    .force.mapRight(_.size).p\nSeq(\"foo\", \"bar\").map(_.associateRight(_.size)).force.map.p\n  // returns: Map(\"foo\" -\u003e 3, \"bar\" -\u003e 3)\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nGroup by key:\n\u003ca name=\"221004113246\"\u003e\u003c/a\u003e\u003ca name=\"group-by-key\"\u003e\u003c/a\u003e\n\n```scala\nSeq(\"foo\" -\u003e 1, \"bar\" -\u003e 2, \"foo\" -\u003e 3).groupByKey.p\n  // returns: Map(bar -\u003e List(2), foo -\u003e List(1, 3))\n  \n// if original order must be preserved:\nSeq(\"bar\" -\u003e 2, \"foo\" -\u003e 1, \"foo\" -\u003e 3).groupByKeyWithListMap.p\n  // returns: ListMap(bar -\u003e List(2), foo -\u003e List(1, 3))\n```\n\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nCount by key:\n\u003ca name=\"221004113247\"\u003e\u003c/a\u003e\u003ca name=\"count-by-key\"\u003e\u003c/a\u003e\n\n```scala\nSeq(\"foo\" -\u003e 1, \"bar\" -\u003e 2, \"foo\" -\u003e 3).countByKey.p\n  // returns: List((2,foo), (1,bar))\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nCount by self:\n\u003ca name=\"221004113237\"\u003e\u003c/a\u003e\u003ca name=\"count-by-self\"\u003e\u003c/a\u003e\n\n```scala\nSeq(\"a\", \"b\", \"a\", \"c\").countBySelf.p\n  // returns: Seq((\"a\", 2), (\"b\", 1), (\"c\", 1)))\n  // note: ordered by DESC\n```\n\n\u003c!-- =========================================================================== --\u003e\n### Help with Tuples\n\u003ca name=\"211006113939\"\u003e\u003c/a\u003e\n\nFrom `import aptus.Tuple{2,3,4,5}_`\n\n```scala\n(1, 2).toSeq.p // Seq(1, 2)\n\n(1, 2).mapFirst (_ + 1) // (2, 2)\n(1, 2).mapSecond(_ + 1) // (1, 3)\n\n(1, 2, 3).mapThird(_ + 1) // (1, 2, 4)\n```\n\n\n\u003c!-- =========================================================================== --\u003e\n### Wrapping\n\u003ca name=\"211006113940\"\u003e\u003c/a\u003e\n\n```scala\n\"foo\".in.some .p // Some(\"foo\")\n\"foo\".in.seq  .p // Seq (\"foo\")\n\"foo\".in.list .p // List(\"foo\")\n\"foo\".in.left .p // Left(\"foo\")\n\"foo\".in.right.p // Right(\"foo\")\n// also see in.someIf/in.noneIf above\n```\n\n\n\u003c!-- =========================================================================== --\u003e\n### Sliding pairs\n\u003ca name=\"211006113941\"\u003e\u003c/a\u003e\n\n```scala\nSeq[Int]()             .slidingPairs // Seq()\nSeq     (1)            .slidingPairs // Seq()\nSeq     (1, 2, 3, 4, 5).slidingPairs // Seq((1, 2), (2, 3), (3, 4), (4, 5))\n\nSeq(1, 2, 3).slidingPairsWithPrevious.p // List((None,1), (Some(1),2), (Some(2),3))\nSeq(1, 2, 3).slidingPairsWithNext    .p // List((1,Some(2)), (2,Some(3)), (3,None))\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\n\u003ca name=\"211006141244\"\u003e\u003c/a\u003e\nconsider the pure stdlib alternative:\n\n```scala\nSeq(1, 2, 3, 4, 5)\n  .sliding(2)\n  .map { x =\u003e\n    (x(0), x(1)) }\n  .toSeq\n```\n\n\u003c!-- =========================================================================== --\u003e\n### Closing resources\n\u003ca name=\"211005133508\"\u003e\u003c/a\u003e\u003ca name=\"closeabled\"\u003e\u003c/a\u003e\n\nAptus' `Closeabled` boils down to:\n\n  `  class Closeabled[T](underlying: T, cls: Closeable) extends Closeable`\n\nConvenient for instance when you don't want to manage pairs of `Iterator/Closeable`, e.g.:\n\n```scala\n// let's write lines\nSeq(\"hello\", \"world\").writeFileLines(\"/tmp/lines\")\n\n// and stream them back\nval myCloseabled: SelfClosingIterator[String] =\n  \"/tmp/lines\".streamFileLines()\n\n// for instance, we can consume the content (will automatically close)\nmyCloseabled                   .consume(_.toList).p // as is\n\u003cXOR\u003e\nmyCloseabled.map(_.map(_.size)).consume(_.toList).p // line pre-processing\n```\n\n\u003c!-- =========================================================================== --\u003e\n### Orphan methods\n\u003ca name=\"211006113942\"\u003e\u003c/a\u003e\n\nWe call some method directly from the `aptus` package object if no natural parent can be used.\n\n```scala\naptus.fs.homeDirectoryPath().p // \"/home/tony\"\naptus.hardware.totalMemory().p // 1011351552\naptus.random.uuidString()   .p // a1bffc1e-72aa-477e-ac84-e4133ffcafad\naptus.time.stamp().p           // 240224152753\n\naptus.illegalState   (\"freeze!\") // Exception in thread \"main\" IllegalStateException: freeze!\naptus.illegalArgument(\"freeze!\") // Exception in thread \"main\" IllegalArgumentException: freeze!\n\naptus.reflect.formatStackTrace().p // returns:\n/*\n  java.lang.Throwable\n      at aptus.aptmisc.Reflect$.formatStackTrace(Misc.scala:62)\n      ...\n      \u003cwhere you are in your code\u003e\n*/\n\n// ... (see more in aptus.AptusAliases)\n```\n\n\u003c!-- =========================================================================== --\u003e\n### Conveying intent\n\u003ca name=\"211006113943\"\u003e\u003c/a\u003e\n\nThese are often used to save/homogenize comments.\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nSometimes we want to convey that a sequence cannot be reordered without consequences, think of it as built-in comment\n```\n@ordermatters val mySeq(MostImportant, SecondMostImportant, ...)\n```\n\nAn annotation is favored over a type alias here so that it can be applied to other code areas than sequences.\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nThe following are just aliases, cheap replacements for `NonEmptyList`-like alternatives:\n```scala\nval      values: Nes[Int] =      Seq(1, 2, 3)\nval maybeValues: Pes[Int] = Some(Seq(1, 2, 3))\n```\n\nNote: Value classes don't accept `require` statements\n\n\n\u003c!-- =========================================================================== --\u003e\n### IO\n\u003ca name=\"210531093427\"\u003e\u003c/a\u003e\u003ca name=\"files-handling\"\u003e\u003c/a\u003e\n\nPlain files:\n```scala\n\"hello world\".writeFileContent(\"/tmp/content\")\n\"/tmp/content\".readFileContent().p // prints: \"hello world\"\n\nSeq(\"hello\", \"world\").writeFileLines(\"/tmp/lines\")\n\"/tmp/lines\".readFileLines().p // prints: Seq(\"hello\", \"world\")\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nCompressed files:\n\u003ca name=\"211006113445\"\u003e\u003c/a\u003e\u003ca name=\"compression\"\u003e\u003c/a\u003e\n\n```scala\n\"hello world\".writeFileContent(\"/tmp/content.gz\")\n\"/tmp/content.gz\".readFileContent().p // prints: \"hello world\" \n\nSeq(\"hello\", \"world\").writeFileLines(\"/tmp/lines.gz\")\n\"/tmp/lines.gz\".readFileLines().p // prints: Seq(\"hello\", \"world\")\n\n// note: file -i /tmp/content.gz\" shows it's indeed application/gzip\n\n\"/data/bigfile.gz\".streamFileLines() // returns a SelfClosingIterator[String], which closes itself once all lines have been seen\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nJSON:\n\u003ca name=\"211006113436\"\u003e\u003c/a\u003e\u003ca name=\"json\"\u003e\u003c/a\u003e\n\nA special note about JSON, owing to its ubiquity (and despite its [many flaws](https://github.com/galliaproject/gallia-docs/blob/master/json.md)).\nWhile [Gallia](https://github.com/galliaproject/gallia-core) is my main project pertaining to data in general (especially transformation thereof), I included a minimal set of functionality in Aptus:\n\n```scala\n\"\"\" {\"foo\": 1} \"\"\".jsonObject // returns a com.google.gson.JsonObject\n\"\"\"[{\"foo\": 1}]\"\"\".jsonArray  // returns a com.google.gson.JsonArray\n\n\"\"\"{\"foo\": 1, \"bar\": true}\"\"\".prettyJson.p // .compactJson is also available\n/*\n{\n  \"foo\": 1,\n  \"bar\": true\n}\n*/\n```\n\nIn the future, a subset of Gallia will be created, \nwhich will basically offer a similar set of operations \nbut without any concern for the underlying schema: \n[gallia-dyn](https://github.com/galliaproject/gallia-dyn).\nIt will offer a convenient way to perform \"dynamic\" transformations, \nand therefore handle JSON. Once ready, \na subset of gallia-dyn` will likely be included in Aptus for convenience, \nso that simple manipulations such as these will be possible OOTB:\n\n```scala\n\"\"\"{\"foo\": \"hello\", \"bar\": 2, \"baz\": true}\"\"\"\n  .readObj\n    .toUpperCase(\"foo\")\n    .increment  (\"bar\")\n    .drop       (\"baz\")\n  .printCompactJson()\n  // \"\"\"{\"foo\": \"HELLO\", \"bar\": 3}\"\"\"\n```\n\n\u003c!-- --------------------------------------------------------------------------- --\u003e\nURLs:\n\u003ca name=\"211006113446\"\u003e\u003c/a\u003e\u003ca name=\"urls\"\u003e\u003c/a\u003e\n\n```scala\nval TestResources =\n  \"https://raw.githubusercontent.com/aptusproject/aptus-core/6f4acbc/src/test/resources\"\n\ns\"${TestResources}/content\".readUrlContent() // prints \"hello word\"\ns\"${TestResources}/lines\"  .readUrlLines().p // prints: Seq(\"hello\", \"world\")\n```\n\nNotes:\n- These may move under `\"...\".file` and `\"...\".url` respectively (TBD)\n- In the future we'll allow a basic POST as well\n\n\u003c!-- =========================================================================== --\u003e\n### File System\n\u003ca name=\"210531093417\"\u003e\u003c/a\u003e\u003ca name=\"file-system\"\u003e\u003c/a\u003e\n\nA very lightweight way to handle the file system, not mean to be comprehensive (use [`os-lib`](https://github.com/com-lihaoyi/os-lib) for more power)\n```scala\n\"/tmp/sbt\".path.isDir()\n\"/tmp/sbt\".path.file.removeFile()\n...\n\n\"/tmp/sbt\".path.dir.listNames()\n...\n\"/tmp/sbt\".path.dir.listFilePathsRecursively()\n```\n\n\u003c!-- =========================================================================== --\u003e\n### System calls\n\u003ca name=\"210601115320\"\u003e\u003c/a\u003e\u003ca name=\"system-calls\"\u003e\u003c/a\u003e\n\nQuick-and-dirty system calls:\n\n```scala\n\"echo hello\"           .systemCall() // prints: \"hello\"\n\"date +%s\"             .systemCall() // prints: \"1622562984\"\n\"head -1 /proc/cpuinfo\".systemCall() // prints: \"processor: 0\"\n```\n\n\u003c!-- =========================================================================== --\u003e\n## Backlog\n\u003ca name=\"210531093429\"\u003e\u003c/a\u003e\n- At least a `List_` counterpart to `Seq_`, maybe via code generation (again see [discussion](https://users.scala-lang.org/t/seq-vs-list-which-should-i-choose/5412) on _Scala Users_)\n- Add more useful abstractions borrowed from other languages, e.g. Python's [`Counter`](https://github.com/anthony-cros/data-science-from-scratch-scala/blob/7ed4a38/src/main/scala/scratchscala/utils/Counter.scala)\n- Lots more tests to be written, though many methods in aptus are too trivial to warrant a test, e.g. `def pipeIf(test: Boolean)(f: A =\u003e A): A = if (test) f(a) else a`\n- More useful methods remain to be ported from Aptus' prototype (not published because too messy)\n- See all the `TODO`s in the code\n- Also see Gallia's [backlog](https://github.com/galliaproject/gallia-docs/blob/master/tasks.md#aptus)\n\n\u003c!-- =========================================================================== --\u003e\n## Contributing\n\u003ca name=\"210531093430\"\u003e\u003c/a\u003e\n\nContributions welcome.\n\n\n\u003c!-- =========================================================================== --\u003e\n","funding_links":[],"categories":["Table of Contents"],"sub_categories":["Misc"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faptusproject%2Faptus-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faptusproject%2Faptus-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faptusproject%2Faptus-core/lists"}