diff --git a/src/bootsupport/modules/README.md b/src/bootsupport/modules/README.md index ed6e9672..127aed2b 100644 --- a/src/bootsupport/modules/README.md +++ b/src/bootsupport/modules/README.md @@ -1,24 +1,24 @@ -This is primarily for tcl .tm modules required for your bootstrapping/make/build process. -It could include other files necessary for this process. - -The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries. - -The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src. -The modules can be your own, or 3rd party such as individual items from tcllib. - -You can copy modules from a running punk shell to this location using the dev command. - -e.g -dev lib.copyasmodule some::module::lib bootsupport - -The dev command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package. - -e.g the result might be a file such as -/src/bootsupport/some/module/lib-0.1.tm - -The originating library may not yet be in .tm form. -You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only. - -Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works. - - +This is primarily for tcl .tm modules required for your bootstrapping/make/build process. +It could include other files necessary for this process. + +The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries. + +The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src. +The modules can be your own, or 3rd party such as individual items from tcllib. + +You can copy modules from a running punk shell to this location using the dev command. + +e.g +dev lib.copyasmodule some::module::lib bootsupport + +The dev command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package. + +e.g the result might be a file such as +/src/bootsupport/some/module/lib-0.1.tm + +The originating library may not yet be in .tm form. +You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only. + +Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works. + + diff --git a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index 4f108187..8384197a 100644 --- a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -433,20 +433,26 @@ namespace eval punk::mix::commandset::project { #scan all files in template # #TODO - deck command to substitute templates? - set templatefiles [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] + set templateinfo_list [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] set stripprefix [file normalize $layout_path] set tagmap [list [lib::template_tag project] $projectname] - if {[llength $templatefiles]} { + if {[llength $templateinfo_list]} { puts stdout "Filling template file placeholders with the following tag map:" foreach {placeholder value} $tagmap { puts stdout " $placeholder -> $value" } } - foreach templatefullpath $templatefiles { + foreach templateinfo $templateinfo_list { + lassign $templateinfo templatefullpath template_tagnames_found set templatetail [punk::repo::path_strip_alreadynormalized_prefixdepth $templatefullpath $stripprefix] - set fpath [file join $projectdir $templatetail] + foreach t $template_tagnames_found { + if {"%$t%" ni [dict keys $tagmap]} { + puts stderr "warning: No substitution available for tag: %$t% in $fpath" + } + } + if {[file exists $fpath]} { set fd [open $fpath r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data2 [string map $tagmap $data] @@ -458,7 +464,6 @@ namespace eval punk::mix::commandset::project { puts stderr "warning: Missing template file $fpath" } } - #todo - tag substitutions in src/doc tree ::cd $projectdir diff --git a/src/bootsupport/modules/test/tomlish-1.1.1.tm b/src/bootsupport/modules/test/tomlish-1.1.1.tm deleted file mode 100644 index d365bab1..00000000 Binary files a/src/bootsupport/modules/test/tomlish-1.1.1.tm and /dev/null differ diff --git a/src/bootsupport/modules/test/tomlish-1.1.3.tm b/src/bootsupport/modules/test/tomlish-1.1.3.tm deleted file mode 100644 index 8afb43d9..00000000 Binary files a/src/bootsupport/modules/test/tomlish-1.1.3.tm and /dev/null differ diff --git a/src/make.tcl b/src/make.tcl index 2deda8a3..8cb8153c 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -3006,6 +3006,8 @@ foreach vfstail $vfs_tails { #'archive' based zip offsets - editable in 7z,peazip file copy $raw_runtime $buildfolder/$vfsname.new + #runtime in runtime folder may not have write perm set - ensure the copy does as we need to append + catch {exec chmod +w $buildfolder/$vfsname.new} file delete $buildfolder/$vfsname.zip if {[info commands ::tcl::zipfs] ne ""} { @@ -3073,6 +3075,7 @@ foreach vfstail $vfs_tails { #copy the version that is mounted in this runtime to vfsname.new if {[catch { file copy -force $building_runtime $buildfolder/$vfsname.new + catch {exec chmod +x $buildfolder/$vfsname.new} } errM]} { puts stderr "$kit_type 'file copy -force $building_runtime $buildfolder/$vfsname.new' failed\n$errM" error $errM diff --git a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm index 4b2ae5cf..aecbc39c 100644 --- a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm @@ -433,20 +433,26 @@ namespace eval punk::mix::commandset::project { #scan all files in template # #TODO - deck command to substitute templates? - set templatefiles [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] + set templateinfo_list [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] set stripprefix [file normalize $layout_path] set tagmap [list [lib::template_tag project] $projectname] - if {[llength $templatefiles]} { + if {[llength $templateinfo_list]} { puts stdout "Filling template file placeholders with the following tag map:" foreach {placeholder value} $tagmap { puts stdout " $placeholder -> $value" } } - foreach templatefullpath $templatefiles { + foreach templateinfo $templateinfo_list { + lassign $templateinfo templatefullpath template_tagnames_found set templatetail [punk::repo::path_strip_alreadynormalized_prefixdepth $templatefullpath $stripprefix] - set fpath [file join $projectdir $templatetail] + foreach t $template_tagnames_found { + if {"%$t%" ni [dict keys $tagmap]} { + puts stderr "warning: No substitution available for tag: %$t% in $fpath" + } + } + if {[file exists $fpath]} { set fd [open $fpath r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data2 [string map $tagmap $data] @@ -458,7 +464,6 @@ namespace eval punk::mix::commandset::project { puts stderr "warning: Missing template file $fpath" } } - #todo - tag substitutions in src/doc tree ::cd $projectdir diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index fd30f208..8cb8153c 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.basic/src/make.tcl @@ -375,6 +375,7 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { set support_contents_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths {*}$sourcesupport_module_paths {*}$sourcesupport_library_paths] { #set contents [glob -nocomplain -dir $p -tail *] + if {![file exists $p]} {continue} set contents [punkboot::lib::folder_nondotted_children $p] set readmeposn [lsearch -nocase $contents readme.md] #don't assume 'ledit' available @@ -3005,6 +3006,8 @@ foreach vfstail $vfs_tails { #'archive' based zip offsets - editable in 7z,peazip file copy $raw_runtime $buildfolder/$vfsname.new + #runtime in runtime folder may not have write perm set - ensure the copy does as we need to append + catch {exec chmod +w $buildfolder/$vfsname.new} file delete $buildfolder/$vfsname.zip if {[info commands ::tcl::zipfs] ne ""} { @@ -3072,6 +3075,7 @@ foreach vfstail $vfs_tails { #copy the version that is mounted in this runtime to vfsname.new if {[catch { file copy -force $building_runtime $buildfolder/$vfsname.new + catch {exec chmod +x $buildfolder/$vfsname.new} } errM]} { puts stderr "$kit_type 'file copy -force $building_runtime $buildfolder/$vfsname.new' failed\n$errM" error $errM diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index 4f108187..8384197a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -433,20 +433,26 @@ namespace eval punk::mix::commandset::project { #scan all files in template # #TODO - deck command to substitute templates? - set templatefiles [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] + set templateinfo_list [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] set stripprefix [file normalize $layout_path] set tagmap [list [lib::template_tag project] $projectname] - if {[llength $templatefiles]} { + if {[llength $templateinfo_list]} { puts stdout "Filling template file placeholders with the following tag map:" foreach {placeholder value} $tagmap { puts stdout " $placeholder -> $value" } } - foreach templatefullpath $templatefiles { + foreach templateinfo $templateinfo_list { + lassign $templateinfo templatefullpath template_tagnames_found set templatetail [punk::repo::path_strip_alreadynormalized_prefixdepth $templatefullpath $stripprefix] - set fpath [file join $projectdir $templatetail] + foreach t $template_tagnames_found { + if {"%$t%" ni [dict keys $tagmap]} { + puts stderr "warning: No substitution available for tag: %$t% in $fpath" + } + } + if {[file exists $fpath]} { set fd [open $fpath r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data2 [string map $tagmap $data] @@ -458,7 +464,6 @@ namespace eval punk::mix::commandset::project { puts stderr "warning: Missing template file $fpath" } } - #todo - tag substitutions in src/doc tree ::cd $projectdir diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index fd30f208..8cb8153c 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl @@ -375,6 +375,7 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { set support_contents_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths {*}$sourcesupport_module_paths {*}$sourcesupport_library_paths] { #set contents [glob -nocomplain -dir $p -tail *] + if {![file exists $p]} {continue} set contents [punkboot::lib::folder_nondotted_children $p] set readmeposn [lsearch -nocase $contents readme.md] #don't assume 'ledit' available @@ -3005,6 +3006,8 @@ foreach vfstail $vfs_tails { #'archive' based zip offsets - editable in 7z,peazip file copy $raw_runtime $buildfolder/$vfsname.new + #runtime in runtime folder may not have write perm set - ensure the copy does as we need to append + catch {exec chmod +w $buildfolder/$vfsname.new} file delete $buildfolder/$vfsname.zip if {[info commands ::tcl::zipfs] ne ""} { @@ -3072,6 +3075,7 @@ foreach vfstail $vfs_tails { #copy the version that is mounted in this runtime to vfsname.new if {[catch { file copy -force $building_runtime $buildfolder/$vfsname.new + catch {exec chmod +x $buildfolder/$vfsname.new} } errM]} { puts stderr "$kit_type 'file copy -force $building_runtime $buildfolder/$vfsname.new' failed\n$errM" error $errM diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index 4f108187..8384197a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -433,20 +433,26 @@ namespace eval punk::mix::commandset::project { #scan all files in template # #TODO - deck command to substitute templates? - set templatefiles [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] + set templateinfo_list [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] set stripprefix [file normalize $layout_path] set tagmap [list [lib::template_tag project] $projectname] - if {[llength $templatefiles]} { + if {[llength $templateinfo_list]} { puts stdout "Filling template file placeholders with the following tag map:" foreach {placeholder value} $tagmap { puts stdout " $placeholder -> $value" } } - foreach templatefullpath $templatefiles { + foreach templateinfo $templateinfo_list { + lassign $templateinfo templatefullpath template_tagnames_found set templatetail [punk::repo::path_strip_alreadynormalized_prefixdepth $templatefullpath $stripprefix] - set fpath [file join $projectdir $templatetail] + foreach t $template_tagnames_found { + if {"%$t%" ni [dict keys $tagmap]} { + puts stderr "warning: No substitution available for tag: %$t% in $fpath" + } + } + if {[file exists $fpath]} { set fd [open $fpath r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data2 [string map $tagmap $data] @@ -458,7 +464,6 @@ namespace eval punk::mix::commandset::project { puts stderr "warning: Missing template file $fpath" } } - #todo - tag substitutions in src/doc tree ::cd $projectdir diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index fd30f208..8cb8153c 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl @@ -375,6 +375,7 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { set support_contents_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths {*}$sourcesupport_module_paths {*}$sourcesupport_library_paths] { #set contents [glob -nocomplain -dir $p -tail *] + if {![file exists $p]} {continue} set contents [punkboot::lib::folder_nondotted_children $p] set readmeposn [lsearch -nocase $contents readme.md] #don't assume 'ledit' available @@ -3005,6 +3006,8 @@ foreach vfstail $vfs_tails { #'archive' based zip offsets - editable in 7z,peazip file copy $raw_runtime $buildfolder/$vfsname.new + #runtime in runtime folder may not have write perm set - ensure the copy does as we need to append + catch {exec chmod +w $buildfolder/$vfsname.new} file delete $buildfolder/$vfsname.zip if {[info commands ::tcl::zipfs] ne ""} { @@ -3072,6 +3075,7 @@ foreach vfstail $vfs_tails { #copy the version that is mounted in this runtime to vfsname.new if {[catch { file copy -force $building_runtime $buildfolder/$vfsname.new + catch {exec chmod +x $buildfolder/$vfsname.new} } errM]} { puts stderr "$kit_type 'file copy -force $building_runtime $buildfolder/$vfsname.new' failed\n$errM" error $errM diff --git a/src/project_layouts/vendor/punk/project-0.1/src/bootsupport/modules/README.md b/src/project_layouts/vendor/punk/project-0.1/src/bootsupport/modules/README.md index ed6e9672..127aed2b 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/bootsupport/modules/README.md +++ b/src/project_layouts/vendor/punk/project-0.1/src/bootsupport/modules/README.md @@ -1,24 +1,24 @@ -This is primarily for tcl .tm modules required for your bootstrapping/make/build process. -It could include other files necessary for this process. - -The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries. - -The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src. -The modules can be your own, or 3rd party such as individual items from tcllib. - -You can copy modules from a running punk shell to this location using the dev command. - -e.g -dev lib.copyasmodule some::module::lib bootsupport - -The dev command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package. - -e.g the result might be a file such as -/src/bootsupport/some/module/lib-0.1.tm - -The originating library may not yet be in .tm form. -You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only. - -Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works. - - +This is primarily for tcl .tm modules required for your bootstrapping/make/build process. +It could include other files necessary for this process. + +The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries. + +The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src. +The modules can be your own, or 3rd party such as individual items from tcllib. + +You can copy modules from a running punk shell to this location using the dev command. + +e.g +dev lib.copyasmodule some::module::lib bootsupport + +The dev command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package. + +e.g the result might be a file such as +/src/bootsupport/some/module/lib-0.1.tm + +The originating library may not yet be in .tm form. +You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only. + +Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works. + + diff --git a/src/vendormodules/dollarcent-1.1.tm b/src/vendormodules/dollarcent-1.1.tm new file mode 100644 index 00000000..4c69c854 --- /dev/null +++ b/src/vendormodules/dollarcent-1.1.tm @@ -0,0 +1,1522 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2025 +# +# @@ Meta Begin +# Application dollarcent 1.1 +# Meta platform tcl +# Meta license MIT +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin dollarcent_module_dollarcent 0 1.1] +#[copyright "2025"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require dollarcent] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of dollarcent +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by dollarcent +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6-}] + + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +namespace eval dollarcent { + #only export main api - see at end of namespace + #todo - tidy up functions in main namespace and move some to lib etc + #namespace export {[a-z]*}; # Convention: export all lowercase + #variable xyz + + #*** !doctools + #[subsection {Namespace dollarcent}] + #[para] Core API functions for dollarcent + #[list_begin definitions] + + variable strict 1 ;#0|1|2 + # 0 = ignore intermixing of conversions with different fractional-cent units. + # 1 = warn with msg to stderr about intermixing + # 2 = raise error + + #2025 - todo + #WARNING - statefulness of variables such as strict,units,rounding_method - suggest we should use an object-like interface - otherwise concurrent use in different contexts is risky. + + variable units "" ;#last active units used for fractional-cents. Raise error or warning if attempt to use a different conversion unit without explicitly resetting. + + proc units {{fractional_cent_unit "-"}} { + variable units + if {$fractional_cent_unit eq "-"} { + return $units + } else { + if {$fractional_cent_unit eq ""} { + #allow 'reset' so that whatever next unit is used in conversion will become next units value. + set units "" + } else { + foreach {a b} {c c-0 xc c-1 cc c-2 mc c-3 _xc c-4 _cc c-5 _mc c-6} { + if {$fractional_cent_unit eq $a} { + set fractional_cent_unit $b + break + } + } + if {![string match "c-*" $fractional_cent_unit]} { + error "Expected fractional cent unit such as 'c' 'xc' 'mc' '_xc' or 'c-1' 'c-2' 'c-3' 'c-4' etc - got: '$fractional_cent_unit'" + } + set units $fractional_cent_unit + } + } + } + + variable rounding_methods + + set rounding_methods [dict create] + dict set rounding_methods halfeven {unbiased statisticians bankers convergent dutch gaussian} + dict set rounding_methods halfup {up euro} + dict set rounding_methods halfdown {down} + dict set rounding_methods halfawayfromzero {} + dict set rounding_methods halftowardszero {} + dict set rounding_methods unsupported {stochastic swedish cash} + #'cash' method (sometimes called swedish) depends on smallest unit of currency in play for a particular jurisdication/country + + variable rounding_method halfup ;#default rounding 0.5 upwards + + proc roundingmethods {} { + variable rounding_methods + return $rounding_methods + } + + #--------------------------------- + #rounding implementations + + proc do_round_halfeven {num e1 e2} { + if {($e1 eq "5") && ($e2 eq "0")} { + set last [string range $num end end] + if {($last % 2) == 0} { + #even + incr num + } + } elseif {$e1 eq "5"} { + incr num + } + return $num + } + + proc do_round_halfup {num e1 e2} { + if {$e1 eq "5"} { + incr num + } + return $num + } + proc do_round_halfdown {num e1 e2} { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + # ${num}5x is > ${num}50 round up. + incr num + } + #exactly ${num}50 - leave num as is. (ie round down) + } + return $num + } + proc do_round_halfawayfromzero {num e1 e2} { + if {$num > 0} { + set num [do_round_halfup $num $e1 $e2] + } else { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + incr num -1 + } + } + } + return $num + } + proc do_round_halftowardszero {num e1 e2} { + if {$num > 0} { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + incr num -1 + } + } + } else { + set num [do_round_halfup $num $e1 $e2] + } + return $num + } + #---------------------------------- + proc do_round {num e1 e2} { + variable rounding_method + variable rounding_methods + set meth "-" + if {$rounding_method ni [dict keys $rounding_methods]} { + foreach m [dict keys $rounding_methods] { + set aliases [dict get $rounding_methods $m] + if {$rounding_method in $aliases} { + if {"::dollarcent::do_round_$m" in [info commands ::dollarcent::*]} { + set meth $m + } + break + } + } + } else { + set meth $rounding_method + } + if {$meth eq "-"} { + error "rounding method '$rounding_method' not supported" + } + + do_round_$meth $num $e1 $e2 + } + + #2010-04 - roundingmethod not yet in use. review. Get the basic roundhalfup version working and fully tested first! + proc roundingmethod {args} { + variable rounding_method + variable rounding_methods + if {![llength $args]} { + return $rounding_method + } else { + set allmethods [list] + foreach m [dict keys $rounding_methods] { + if {$m ne "unsupported"} { + lappend allmethods $m + lappend allmethods {*}[dict get $rounding_methods $m] + } + } + + set desiredmethod [lindex $args 0] + if {$desiredmethod ni $allmethods} { + error "rounding method '$desiredmethod' is not supported" + } + set rounding_method $desiredmethod + } + } + + + set ns [namespace current] + + + #proc convert {amount source TO target args} {} ;#old v 1.0 signature - not easy to alias or pipeline + package require punk::args + punk::args::define { + @id -id ::dollarcent::convert + @cmd -name dollarcent::convert -help\ + "Convert between a floating point number representing a value specified in the + major unit of a currency - to an integer representing the number of fractional + parts of the minor unit. + e.g if dealing with dollars and cents and using c-level cc (c-2) + convert between a dollar figure like 1.00 + and a quantity of fractional-cents 10000" + @form -form dollar + @leaders -min 3 -max 3 + double_amount_type -type string -choices {dollars} -help\ + "'dollars' means convert FROM the large unit of whatever currency is + being processed" + to -type literal(TO)|literal(to) + target_type -type string -choicerestricted 0 -choices {c xc cc mc _xc _cc _mc}\ + -choicelabels { + "c"\ + "equiv: c-0" + "xc"\ + "equiv: c-1" + "cc"\ + "equiv: c-2" + "mc"\ + "equiv: c-3" + "_xc"\ + "equiv: c-4" + "_cc"\ + "equiv: c-5" + "_mc"\ + "equiv: c-6" + } -help\ + "The scale of the small unit of currency for calculation purposes. + e.g c (c-0) is 1 major unit to 100 + mc (c-3) is 1 major unit to 100_000 + _mc (c-6) is 1 major unit to 100_000_000 + + c happens to correspond to USD $1 to 100c + and _mc happens to correspond to 1BTC to 100Million Satoshi, + but generally you'd want a higher level than the number of + small units per major unit so that rounding errors are reduced + to well below the level of the minor currency unit. + " + @opts + -strict -type integer -default 1 -choices {0 1 2}\ + -choicelabels { + 0 {Silence even if units (c-level) used don't match last used} + 1 {Warn on stderr if units have changed} + 2 {Error if units have changed} + } -choicecolumns 1 + @values -min 1 -max 1 + amount -type double -help\ + "Quantity of the largest units of the currency (a conceptual 'Dollar' amount). + For another currency this might be Euros or BTC. + It is specified as a floating-point number with at least 2 decimal places, + but the maximum allowed decimal places is determined by the target_type." + + + @form -form cent + @leaders -min 3 -max 3 + integer_amount_type -type string -choicerestricted 0 -choices {c xc cc mc _xc _cc _mc}\ + -choicelabels { + "c"\ + "equiv: c-0" + "xc"\ + "equiv: c-1" + "cc"\ + "equiv: c-2" + "mc"\ + "equiv: c-3" + "_xc"\ + "equiv: c-4" + "_cc"\ + "equiv: c-5" + "_mc"\ + "equiv: c-6" + } -help\ + "The scale of the small unit of currency for calculation purposes. + e.g c (c-0) is 1 major unit to 100 + mc (c-3) is 1 major unit to 100_000 + _mc (c-6) is 1 major unit to 100_000_000 + + c happens to correspond to USD $1 to 100c + and _mc happens to correspond to 1BTC to 100Million Satoshi, + but generally you'd want a higher level than the number of + small units per major unit so that rounding errors are reduced + to well below the level of the minor currency unit. + " + to -type literal(TO)|literal(to) + target_type -type string -choices {dollars} -help\ + "'dollars' means convert TO the large unit of whatever currency is + being processed" + + @opts + -strict -type integer -default 1 -choices {0 1 2}\ + -choicelabels { + 0 {Silence even if units (c-level) used don't match last used} + 1 {Warn on stderr if units have changed} + 2 {Error if units have changed} + } -choicecolumns 1 + @values -min 1 -max 1 + amount -type double -help\ + "Integer quantity being used for conversion to the major unit amount. + The same scale (c-level) must be used in converting to and from large + and small units for any calculations to make sense." + + + } + proc convert {args} { + set amount_type [tcl::prefix::match -error "" {dollars} [lindex $args 0]] + if {$amount_type eq "dollars"} { + set argd [punk::args::parse $args -form dollar withid ::dollarcent::convert] + } else { + set amount_type [lindex $args 0] + set argd [punk::args::parse $args -form cent withid ::dollarcent::convert] + } + + variable strict ;#whether and how to alert programmer if subsequent calls use different fractional-cent units. + variable units ;#we remember the fractional cent units used in previous calls and disallow different units unless explicitly set with dollarcent::units. + + lassign [dict values $argd] leaders opts values received + + set bestrict [dict get $opts -strict] + + set target_type [dict get $leaders target_type] + set amount [dict get $values amount] + + #c-level aliases + set c_aliases [dict create c c-0 xc c-1 cc c-2 mc c-3 _xc c-4 _cc c-5 _mc c-6] + + #puts stdout "->convert $amount $amount_type $TO $target" + if {$amount_type eq {dollars}} { + # where c-0 = cents, c-1 represents cents * 10-1 or 'tenths of a cent', c-2 = 'hundredths of a cent' etc. + #c, xc, cc, mc are shortcuts for these - designed to match the convenience functions dc2c dc2xc dc2cc dc2mc dc2_xc, c2dc xc2dc mc2dc etc + # ie roman numeralesque + # x = 10 + # c = 100 (leading char only - trailing one means cents!) + # m = 1000 + # _x = 10000 (underscore to represent 'overbar' used in roman numerals to indicate multiplication by 1000) + # _c = 100000 + # _m = 1000000 + + #dc2_xc dc2_mc + if {[dict exists $c_aliases $target_type]} { + set target_type [dict get $c_aliases $target_type] + } + + if {$units eq ""} { + #1st use in this interp - ok - just set the units. + set units $target_type + } else { + if {$target_type ne $units} { + set msg "The fractional-cent unit already in use is '$units' but the current conversion specified: '$target_type'. Call 'dollarcent::units ' if you wish to change the size of the fractional-cent in use." + append msg "\ne.g 'c-0' = cents, 'c-1' or 'xc' = tenths of a cent, 'c-2' or 'cc' = hundredths of a cent, 'c-3' or 'mc' = thousandths, 'c-4' or '_xc = 10-thousandths etc." + if {$bestrict == 1} { + puts stderr $msg + } elseif {$bestrict == 2} { + error $msg + } else { + #ignore - but *don't* reset units - this may have been a 'oneshot' use of a different unit by use of the -strict 0 option to 'convert'. + #We require that the programmer specifically set the units. + } + } + } + + + set dollars $amount + + lassign [split $target_type -] _c power + #if {$power > 20} { + # error "dollarcent package probably can't handle such small fractions of a cent. '10 to the -20' may be the limit. Check code in dollarcent module and adjust this test as necessary." + #} + + #puts stdout "power is $power" + #puts stdout "dollars: $dollars" + set format "%lld.%1d%1d[string repeat %1d $power]%s" ;#note - this is lower case ll not numeral 11! + + set vars [lrepeat [expr {$power + 2}] _c] ;#just a list of placeholder vars for the scan function to + + set partcount [scan $dollars $format bucks {*}$vars _disallowed] + set dpcount [expr {$partcount -1}] ;#subtract the LHS to get the count of number of decimal places. + if {$dpcount < 2 || ($dpcount > (2 + $power))} { + return -code error "Incorrect precision for target type '$target_type'. Dollar amount must be a decimal number with between 2 and [expr {2 + $power}] digits following the decimal point." + } + + #------------------------------- + #Note: We could do this.. but this is a floating point multiplication. + # - for $power = 0, this seems to work up to about $(2**45).34 ie 35trillion,184billion,372million,88thousand,832 dollars + # - if the chosen base unit is 10 thousandths of a cent, then rounding errors will occur at somewhere around 550billion dollars. + # - Presumably rounding errors regarding a cent for such large sums are not an issue for the vast majority of use-cases - but it's the intention that this module + # eventually be accurate and flexible enough to be used in all sorts of situations e.g for some pseudo-currency in a virtual-world/game economy or perhaps even in a hyperinflationary environment. + # (No fitness for use in such situations is implied) + #return [expr {round($dollars*100*(10**$power))}] + #------------------------------- + + + set subcents [decmul [str2dec $dollars] [str2dec [expr {100*(10**$power)}]]] + #set result [expr {round([dec2str $subcents])}] ;#don't do this - round will do funny things for large numbers. + set result [dec2str $subcents] + + lassign [split $result .] whole part + set c1 [string range $part 0 0] + if {$c1 >= 5} { + if {$c1 > 5} { + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } else { + #c1 exactly 5 + set tail [string range $part 1 end] + if {([string length $tail] ==0) || ($tail == 0)} { + #entirety of 'part' is equivalent to exactly 0.5 + ##!todo - round based on currently active roundingmethod + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } else { + #there is something non-zero folowing c1 + #therefore round up - no matter what the currently active rounding method. + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } + } + } + return $whole + + } else { + #we're converting fractional cents to dollars & cents. + + if {[string match "-*" $amount]} { + set amount [string range $amount 1 end] + set sign - + } else { + set sign "" + } + + set int_pennyparts $amount + if {[dict exists $c_aliases $amount_type]} { + set amount_type [dict get $c_aliases $amount_type] + } + + if {$units eq ""} { + #1st use in this interp - ok - just set the units. + set units $amount_type + } else { + if {$amount_type ne $units} { + set msg "The fractional-cent (c-level) unit already in use is '$units' but the current conversion specified: '$amount_type'. Call 'dollarcent::units ' if you wish to change the size of the fractional-cent in use." + append msg "\ne.g 'c-0' = cents, 'c-1' or 'xc' = tenths of a cent, 'c-2' or 'cc' = hundredths of a cent, 'c-3' or 'mc' = thousandths, 'c-4' or '_xc = 10-thousandths etc." + if {$bestrict == 1} { + puts stderr $msg + } elseif {$bestrict == 2} { + error $msg + } else { + #ignore - but *don't* reset units - this may have been a 'oneshot' use of a different unit by use of the -strict 0 option to 'convert'. + #We require that the programmer specifically set the units. + } + } + } + + + + lassign [split $amount_type -] _c power + if {$power > 19} { + error "dollarcent package probably can't handle such small fractions of a cent. '10 to the -19' may be the limit. Check code in dollarcent module and adjust this test as necessary." + } + + + if {$target_type ne {dollars}} { + error "For conversion amount_type '$amount_type' - unknown target type '$target_type'" + } + #format string note - this is lowercase ll not number 11! + if {[scan $int_pennyparts %lld%c int_pennyparts _disallowed] == 1} { + #---------------- + #NOTE! 'format %.2f' does implicit rounding - and also varies in behaviour across platforms. This is not suitable for use here. + #return [format %.2f [expr {$int_pennyparts /(100.0 *(10**$power))}]] ;#NOTE - %.2f rounds in a strange manner - and is inconsistent across tcl platforms! + #----------------- + + #----------------- + #return [expr {$int_pennyparts /(100.0 *(10**$power))}] + #----------------- + + set r [decdiv [str2dec $int_pennyparts] [str2dec [expr {100*(10**$power)}]]] + #puts stdout "r -> $r" + + lassign $r _dec significand exp + + #trailing zeroes not useful here + #(normally trailing zeroes are sigfigs) + set r_significand [string reverse $significand] + + set exp2 $exp + foreach z [split $r_significand ""] { + if {$z eq "0"} { + incr exp2 + } else { + break + } + } + set significand2 [string trimright $significand "0"] + + #return [dec2str [list decimal $significand $exp]] ;#dollar result + set result [dec2str [list decimal $significand2 $exp2]] ;#dollar result + + if {[string first . $result] < 0} { + set result $result.00 + } else { + lassign [split $result .] dollarpart centpart + if {[string length $centpart] == 1} { + set centpart ${centpart}0 + } + if {[string length $dollarpart] > 1} { + set dollarpart [string trimleft $dollarpart 0] + } + set result $dollarpart.$centpart + } + return $sign$result + } else { + return -code error "Only an integer number of units can be converted to dollars and cents. Please round before converting. amount:'$int_pennyparts'" + } + } + } + + proc roundcents {dollars} { + lassign [split $dollars .] bucks cents + if {[string length $cents] < 2} { + return -cdoe error "'roundcents' requires that dollar amount be given with at least 2 decimal places" + } + + set decimal_dollaramount [str2dec $dollars] + set decimal_100 [str2dec 100] + set decimal_cents [decmul $decimal_dollaramount $decimal_100] + + set fpcents [dec2str $decimal_cents] + #puts stdout "fpcents-> $fpcents" + + lassign [split $fpcents .] wholecents frac + set frac1 [string range $frac 0 0] + set fractail [string range $frac 1 end] + if {$frac1 >= 5} { + if {$frac1 == 5} { + if {([string length $fractail] == 0) || ($fractail == 0)} { + #entirety of 'frac' is equivalent to .5 + #use active rounding method to round + #!todo - roundingmethods + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } else { + #the x in .5x is non-zero - always round up no matter what rounding method is in effect. + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } + } else { + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } + } + + #set cents [expr {round($fpcents)}] + + set d [c2d -strict 0 $wholecents] + return $d + } + + #normalize a string representing dollars and cents, from a US,AU,NZ etc perspective + #ie doesn't cater for european conventions of comma as decimal separator. + #This normalization from the perspective of this library: + # conversion from other conventions such as European should be done at input/output of whole process. (using locale etc) + + # e.g '$2.1' -> '2.10' + #Must also cope with underscores and commas in combination with leading zeros + #Must not lose decimal places e.g $10.33335 -> 10.33335 + #But we convert both 10. and 10.0 to 10.00 (not correct from a sigfig perspective - but more useful) + proc normaldollars {dollaramount} { + set amount [string trim $dollaramount] + set amount [string map {{-$} {$-}} $amount] ;# -$2.10 equivalent to $-2.10 + if {[string index $amount 0] eq {$}} { + } + set amount [string trimleft $amount {$}] + if {$amount in {"" "-"}} { + error "'$dollaramount' is not in a format that normaldollars can recognize" + } + + if {[string match "-*" $amount]} { + set amount [string range $amount 1 end] + set sign - + } else { + set sign "" + } + if {[string match ".*" $amount]} { + set amount 0$amount + } + if {[string match "*." $amount]} { + set amount ${amount}00 + } + + if {[string first . $amount] < 0} { + set amount $amount.00 + } else { + lassign [split $amount .] dollarpart centpart + if {[string length $centpart] == 1} { + set centpart ${centpart}0 + } + if {[string length $dollarpart] > 1} { + set dollarpart [string trimleft $dollarpart 0] + } + set amount $dollarpart.$centpart + } + + return $sign$amount + } + + + variable scale 38 ;#fairly arbitrary - but 28 supposedly matches that of Python. 'ISO COBOL 2002 standard' uses 32, Transact SQL has a max of 38. + variable maxmantisse [expr {10**$scale}] + variable bndmantisse [expr {10*$maxmantisse}] + proc set_scale {newscale} { + variable scale + variable maxmantisse + variable bndmantisse + + set scale $newscale + set maxmantisse [expr {10**$scale}] + set bndmantisse [expr {10*$maxmantisse}] + } + + + #---------------------------------------------------------------- + # Rescale -- + # Rescale the number (using proper rounding) + # + # Arguments: + # decimal in the form {decimal significand exponent} + # + # Result: + # Rescaled number (as a list) + # + proc decrescale {decimal {newscale ""}} { + variable maxmantisse + variable bndmantisse + + lassign $decimal _decimal significand exponent + + if { abs($significand) <= $maxmantisse } { + return [list decimal $significand $exponent] + } + + set rest [expr {$significand % 10}] ;#for case where $maxmantisse < abs($significand) <= $bndmantisse + while { abs($significand) > $bndmantisse } { + set rest [expr {$significand % 10}] + set significand [expr {$significand/10}] + incr exponent + } + + + if { $rest > 5 } { + if { $significand > 0 } { + incr significand + } else { + incr significand -1 + } + } elseif { $rest == 5 } { + #halfup rounding + if { $significand > 0 } { + incr significand + } else { + incr significand -1 + } + + #bankers rounding + #if { ($significand/10) % 2 == 1 } { + # incr significand + #} + } + + return [list decimal $significand $exponent] + } + + proc str2dec {string} { + set pos [string first . $string] + if {$pos < 0} { + #significand = mantissa + set significand $string + set significand [string trimleft $string "0"] + if {$significand eq ""} { + set significand 0 + } + set exponent 0 + } else { + set fraction [string range $string $pos+1 end] + set significand [string trimleft [string map {. ""} $string] 0] + if {$significand eq ""} { + set significand 0 + } + set exponent [expr {-[string length $fraction]}] + } + return [list decimal $significand $exponent] + } + + proc dec2str {decimal_as_list} { + #puts stdout "==>dec2str $decimal_as_list" + lassign $decimal_as_list _decimal significand exponent + if {![string length $significand]} { + set significand 0 + } + if {$significand < 0} { + set sign - + } else { + set sign "" + } + set significand [string map {- ""} $significand] + if {$exponent > 0} { + if {$significand == 0} { + set string 0 + } else { + set string $sign$significand[string repeat 0 $exponent] + } + } else { + set digits [string length $significand] + + set exponent [expr {abs($exponent)}] + if {$digits >= $exponent} { + if {$exponent == 0} { + set string $sign$significand + } else { + if {$digits == $exponent} { + #e.g 10 * 10**-2 + set lhs "0" + } else { + set lhs [string range $significand 0 [expr {$digits-$exponent-1}]] + } + set string $sign$lhs.[string range $significand [expr {$digits-$exponent}] end] + } + } else { + set string ${sign}0.[string repeat 0 [expr {$exponent-$digits}]]$significand + } + } + return $string + } + + proc multiply_as_decimal {a b args} { + return [dec2str [decmul [str2dec $a] [str2dec $b] {*}$args]] + } + proc divide_as_decimal {a b args} { + return [dec2str [decdiv [str2dec $a] [str2dec $b] {*}$args]] + } + proc add_as_decimal {a b args} { + return [dec2str [decadd [str2dec $a] [str2dec $b] {*}$args]] + } + proc power_as_decimal {a b args} { + return [dec2str [decpow [str2dec $a] [str2dec $b] {*}$args]] + } + + + #round 'val' to 'dp' decimal places + proc dpround {val dp} { + error "not implemented" + } + #round 'val' to 'sigfigs' significant figures + proc sigfiground {val sigfigs} { + error "not implemented" + } + + #significant figures 'are a rather crude way of tracking precision' + # see http://scienceblogs.com/goodmath/2009/03/basics_significant_figures.php + # - NOTE - to review - are sigfigs really even relevant to currency work? + proc sigfigs {floatstr} { + + #1) all non-zero digits are considered significant + #2) zeros between non-zeros are significant. + #3) leading zeros not significant. + + # - for this function - we will assume trailing zeros in numbers with no decimal point are insignificant. + # There is an ambiguity here because it could be that the zeros are significant but the value just happens to be round.. + # we adopt the convention that a trailing . on the input will indicate that the previous zeros are significant. + # e.g "100" has 1 sigfigs but "100." has 3 sigfigs. + + #rules when performing calculations: + #a) for multiplication and division the final answer should contain only as many sigfigs as the number with the *least* number of sigfigs. + #b) for addition and subtraction - keep the number of sigfigs in the input with the smallest number of *decimal places* + + + set i 0 + set s 0 + set in_sigfigs 0 + set sigs "" ;#significant digits + set dp 0 + set in_dp 0 + if {[string first . $floatstr] >= 0} { + set has_radix 1 ;#radix is a more general term for decimal point. + } else { + set has_radix 0 + } + if {[string match "-*" $floatstr]} { + set floatstr [string map {- ""} $floatstr] + incr i + } + foreach c [split $floatstr ""] { + if {$c eq "0"} { + if {$in_sigfigs} { + append sigs "0" + } + } else { + if {$c ne "."} { + if {!$in_sigfigs} { + set in_sigfigs 1 + set s $i + } + append sigs $c + } else { + set in_dp 1 + } + } + if {$c ne "."} { + if {$in_dp} { + incr dp + } + } + incr i + } + if {!$has_radix} { + #no radix. We will consider trailing zeros to be insignificant. + set sigs [string trimright $sigs "0"] + } + + return [list count [string length $sigs] startindex $s digits $sigs dp $dp] + } + + #decimal sum + proc decadd {a b} { + lassign $a _decimal sa ea ;#significand-a (aka mantissa-a), exponent-a + lassign $b _decimal sb eb + + if {$ea > $eb} { + set sa [expr {$sa * 10 ** ($ea-$eb)}] + set er $eb + } else { + set sb [expr {$sb * 10 ** ($eb-$ea)}] + set er $ea + } + set sr [expr {$sa + $sb}] + return [list decimal $sr $er] + } + + proc decpow2 {a b} { + #a**b + lassign $a _decimal sa ea + lassign $b _decimal sb eb + if {$sb eq 0} { + return [list decimal 1 0] + } else { + if {$sb < 0} { + #a**-n = 1/a**n + error "negative exponent not supported - sorry - try plain expr" + + } else { + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + + #powx = intermediate power x + + #sa **(sb * ten_2_eb) * 10**(ea*sb*(ten_2_eb)) + #sa **(sb * ten_2_eb) * 10**(exp1) + #sa **(sb * ten_2_eb) * pow2 + #pow3 * pow2 + + puts stdout "--->sa **(sb * ten_2_eb) * 10**(ea*sb*(ten_2_eb))<---" + puts stdout "--->sa **(sb * ten_2_eb) * 10**(exp1)<---" + + puts stdout "===>[subst {($sa ** ($sb * 10 **$eb)) * (10**($ea*$sb*(10**$eb))) }]<====" + set ten_2_eb [expr {10**$eb}] + puts stdout "->ten_2_eb: $ten_2_eb" + set exp1 [expr {$ea * $sb * $ten_2_eb}] + if {$exp1 < 0 } { + set exp1plus [string map {- ""} $exp1] + set d [expr {10**$exp1plus}] + puts stdout "...10**$exp1plus= $d" + set pow2 [divide_as_decimal 1 $d] + } else { + set pow2 [expr {10**$exp1}] + } + + puts stdout "->exp1: $exp1" + puts stdout "->pow2: $pow2" + + set exp2 [expr {$sb * $ten_2_eb}] + if {$exp2 < 0} { + set exp2plus [string map {- ""} $exp2] + set d [expr {$sa**$exp2plus}] + puts stdout "...$sa**$exp2plus= $d" + set pow3 [divide_as_decimal 1 $d] + } else { + set pow3 [expr {$sa ** $exp2}] + } + + set a1 [multiply_as_decimal $pow3 $pow2] + #return $a1 + return [str2dec $a1] + + set a1 [expr {($sa ** ($sb * 10 **$eb)) * (10**($ea*$sb*(10**$eb))) }] + return [str2dec $a1] + #set er [expr {$ea + $eb}] + + return [list decimal $sr $er] + } + } + } + + #broken + proc decpow {a b} { + puts stdout "decpow $a $b" + puts stderr "REVIEW - noted as 'broken' in source'" + #a**b + #make sure that all 'x ** y' operations within 'expr' involve integer x and positive integer y. + + lassign $a _decimal sa ea + lassign $b _decimal sb eb + if {$sb eq 0} { + return [list decimal 1 0] + } else { + if {$eb < 0} { + set ebplus [expr {abs($eb)}] + #set ten2eb [dec2str [decdiv {decimal 1 0} [str2dec [expr {10**$ebplus}]] ]] + #set x [expr {$sb * $ten2eb}] + set decpow [decpow {decimal 10 0} [list decimal $ebplus 0]] + + set divscale [expr {min(2,[string length [lindex $decpow 1]])}] + puts stdout "!!! decdiv {decimal 1 0} $decpow $divscale" + set ten2eb [decdiv {decimal 1 0} $decpow -dp $divscale] + set x [decmul [list decimal $sb 0] $ten2eb] + } elseif {$eb == 0} { + set x [list decimal $sb 0] + } else { + #set x [expr {$sb * (10 **$eb)}] + set x [decmul [list decimal $sb 0] [decpow {decimal 10 0} [list decimal $eb 0]]] + } + puts stdout "sb * (10 ** eb) -> $sb * (10 ** $eb) ----> x: $x" + + #raw eqn: + # sa*(10**ea) ** sb*(10**eb) + # using rule (xy)**n = (x**n)*(y**n) + # sa**(sb * 10**eb) * (10**ea)**(sb * 10**eb) + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + #sa**(bdec) * 10**(ea*bdec) + + #puts stdout "--->sa **(sb*(10**eb)) * 10**(ea * sb*(10**eb)))<---" + if {$ea == 0} { + puts stdout "------------ ea = 0" + #$a = $sa (since 10**0 = 1) + + #sa **(sb * (10**eb)) + #exponent law: x**mn = (x**m)**n + #(sa ** sb) ** (10**eb) + if {$eb == 0} { + puts stdout "------------ eb = 0 sa: $sa sb: $sb" + return [list decimal [expr {$sa ** $sb}] 0] + } elseif {$eb > 0} { + #sa ** (sb * (10**eb)) + puts stdout "------------ eb > 0" + #set rdec [decpow [str2dec [expr {$sa ** $sb}]] [list decimal 1 $eb]] + set r [expr ($sa ** $sb) ** (10**$eb)] + set a1 [str2dec $r] + return $a1 + + #puts stdout "xxxxxxx eb: $eb" + #return [list decimal [expr {$sa ** $sb}] [expr {10 ** $eb}]] + } else { + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #sa**(sb * 10**eb) * 10**0 + #---------------------------------------------- + puts stdout "------------ eb < 0" + #b = sb * (10** eb) + #if eb negative then + #b = sb * (1/(10**abs(eb))) + #b = sb / (10**abs(eb)) + #negative exponent of number b - therefore equivalent of 1/(sb**abs(eb)) + if {$sa < 0} { + error "negative values not handled here.. sorry" + } + #exponent law: x**(m/n) = nth root of x**m + + #sb * 1/10**abs(eb) + + #set 10eb [decpow {decimal 10 0} [list decimal [expr {abs($eb)}] 0]] + set 10eb [expr {10**abs($eb)}] + set b [decdiv [list decimal $sb 0] [list decimal $10eb 0]] + + puts "zzz" + return [decpow [list decimal $sa 0] $b] + + + + + #puts stdout "about to find '10**abs($eb)'th root of ($sa * (10**$sb))" + #set a1 [str2dec [root [expr {$sa * (10**$sb)}] [expr {10**abs($eb)}]]] + #return $a1 + + #puts stdout "about to root ($sa ** $sb) 10**[expr abs($eb)]" + #set a1 [str2dec [root [expr {$sa**$sb}] [expr {10**abs($eb)}]]] + #return $a1 + } + } else { + puts stdout "nonzero ea = $ea" + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + + #puts stdout "--->sa **(x) * 10**(ea * x)<---" + + #$b < 0 whenever $sb <0 + set xstr [dec2str $x] + if {$sb < 0} { + #raising a to a negative power + + #1/sa**abs($sb * 10**eb) + set xplus [expr {abs($xstr)}] + + puts stdout "a)-->sa ** xplus => $sa ** $xplus" + #set lhs [decdiv {decimal 1 0} [str2dec [expr {$sa ** $xplus}]]] + + #--- + #work out a scale + set p10a [root $sa 10] + set p10b [root $xplus 10] + set sc [multiply_as_decimal $p10a $p10b] + lassign [split $sc .] int frac + set sc [string length $int] + #--- + + set lhs [decdiv {decimal 1 0} [decpow [list decimal $sa 0] [list decimal $xplus 0]] -dp $sc] + + } else { + #> 0 (not = 0 because we've already handled that) + puts stdout "b)" + #set lhs [expr {$sa**$x}] + #set lhs [str2dec [expr {$sa ** $x}]] + if {[string first . $xstr] < 0} { + #puts stdout "-->str2dec $sa ** $xstr" + set lhs [str2dec [expr {$sa ** $xstr}]] ;#integers + } else { + set lhs [decpow [list decimal $sa 0] $x] + } + } + puts stdout "**lhs: $lhs" + + + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + + if {[string first . $xstr] < 0} { + set exp2 [expr {$ea * $xstr}] ;#integers + } else { + set exp2 [dec2str [decmul [list decimal $ea 0] $x]] + } + #puts stdout " ea * x = $ea * $x = $exp2" + if {$exp2 < 0} { + puts stdout "c)" + set exp2plus [expr {abs($exp2)}] + #set d [expr {10**$exp2plus}] + #puts stdout "...10**$exp2plus= $d" + #set rhs [divide_as_decimal 1 $d] + if {$exp2plus > $::dollarcent::scale} { + set sc $exp2plus + } else { + set sc $::dollarcent::scale + } + + set rhs [decdiv {decimal 1 0} [str2dec [expr {10 ** $exp2plus}]] -dp $sc] + + + } else { + puts stdout "d) 10 ** $exp2" + #set rhs [expr {10 ** $exp2}] + set rhs [str2dec [expr {10 ** $exp2}]] + } + puts stdout "**rhs: $rhs" + + set a1 [decmul $lhs $rhs] + return $a1 + #set a1 [multiply_as_decimal $lhs $rhs] + #return [str2dec $a1] + } + } + } + + proc root {num {n 2} args} { + if {[string is integer -strict $n]} { + return [nthroot $num $n {*}$args] + } else { + return [expr {$num ** (1.0/$n)}] + } + } + + + #shifting nth root algorithm. Tested against 1st 50 or so places of root 2 - seems to work. + # - not practical for large values of n. + proc nthroot {num {n 2} args} { + set default [list -dp "" -scale $::dollarcent::scale ] + set opts [dict merge $default $args] + + lassign [split $num "."] int frac + + set dpdesired [dict get $opts -dp] + if {$dpdesired eq ""} { + set dpdesired [string length $frac] + } + + set dpworking [expr {max($dpdesired,[dict get $opts -scale])}] + + if {[string length $frac] < $dpworking} { + lappend frac [string repeat "0" [expr {$dpworking -[string length $frac]}]] + } + + set overlen [expr {[string length $int] % $n}] + if {$overlen > 0} { + set padding [string repeat 0 [expr {$n - $overlen}]] + } else { + set padding "" + } + set int $padding$int ;#padded on left with enough zeros so that we can grab groups of $n digits + + set digits [split $int {}] + + + set frac_digits [split $frac {}] + + set flen [llength $frac_digits] + if {$flen > 0} { + lappend frac_digits {*}[lrepeat [expr {$flen * ($n-1)}] 0] ;#we need another $n-1 digits for every existing digit in the fractional part + + #b + #set overlen [expr {[llength $frac_digits] % $n}] + #if {$overlen > 0} { + # lappend frac_digits {*}[lrepeat [expr {$n-$overlen}] 0] + #} + } + + lappend digits {*}$frac_digits + + + + #set overlen [expr {[string length $frac] % $n}] + #if {$overlen > 0} { + # set padding [string repeat 0 [expr $n - $overlen]] + # #append frac [string repeat "0" $flen] ;#???? + #} else { + # set padding "" + #} + #set frac $frac$padding + + #n = degree of root to be extracted = (10**eb) + #x = radicand processed thus far + #y = root extracted thus far + #r = remainder + #a = next n digits of the radicand + #B = next digit of the root + + #invariant y**n + r = x + #ie. y is the largest integer less than the nth root of x and r is the remainder. + + set x 0 + set y 0 + set r 0 + + #base = 10 - base of number system we're using. + #puts stdout "intfrac: '$int$frac'" + #puts stdout $digits + set nblock "" + foreach d $digits { + append nblock $d + if {[string length $nblock] < $n} { + continue + } + #puts stdout ". $nblock ." + scan $nblock %d a + set nblock "" ;#ready to build nex one. + + #set x2 [expr {(10**$n) * $x + $a}] ;#running power (radicand processed so far) + + #find largest integer B such that (10y + B)**n <= (10**n)x + a + #---- + # for square root case (n =2) + # 100y**2 + 20yB +B**2 <= 100x + a + #---- + #B will always be less than base ie B < 10 (see http://en.wikipedia.org/wiki/Shifting_nth_root_algorithm ) + for {set B 0} {$B < 10} {incr B} { + if {(((10 * $y + $B) ** $n) - (10**$n * $y**$n)) <= (((10**$n) * $r) + $a)} { + #ok + } else { + #overshot + break + } + } + incr B -1 + set y2 [expr {(10 * $y) + $B}] + set r2 [expr {(10**$n)*$r + $a - ((10*$y + $B)**$n - (10**$n)*($y**$n))}] + + set y $y2 + set r $r2 + + #puts -nonewline $B + } + #puts stdout "" + #puts stdout "y: $y" + + #set a [expr {$y / pow(10,([llength $frac_digits]/$n))}] + #puts stdout "answer1: $a" + + #set sc [expr {[llength $frac_digits] / $n}] + #set sc $::dollarcent::scale + set sc $dpdesired + + set y [dec2str [decdiv [str2dec $y] [str2dec [expr {10**([llength $frac_digits]/$n)}] ] -dp $sc] ] + return $y + } + + + #decimal product + proc decmul {a b} { + #puts stdout "decmul $a $b" + lassign $a _decimal sa ea ;#significand-a exponent-a + lassign $b _decimal sb eb + + #integer operations - not floating point. + set sr [expr {$sa * $sb}] + set er [expr {$ea + $eb}] + + return [list decimal $sr $er] + } + + #decimal division + #rounds last digit. (automatically chooses scale large enough to provide additional 2 digits to determine rounding value) + proc decdiv {a b args} { + variable scale + set default [list -dp ""] + set opts [dict merge $default $args] + set dpdesired [dict get $opts -dp] + + if {$dpdesired ne ""} { + #set sc $dpdesired + set dp_exact 1 + } else { + set dpdesired $scale + set dp_exact 0 ;#we're allowed to trim repeat rhs zeros + } + if {$dpdesired >= $scale} { + set sc [expr {$dpdesired + 2}] ;#2 additional digits required for rounding + } else { + if {($scale - $dpdesired) == 1} { + set sc [expr {$dpdesired + 2}] ;#scale is just above dpdesired + } else { + set sc $scale + } + } + #set sc [expr {max($dpdesired,$scale)}] + + lassign $a _decimal sa ea ;#sa significand-a, ea exponent-a + lassign $b _decimal sb eb + + if {$ea >=0} { + set dpa 0 + } else { + set dpa [expr {abs($ea)}] + } + if {$eb >=0} { + set dpb 0 + } else { + set dpb [expr {abs($eb)}] + } + + + set ab_highest_dp [expr {max($dpa,$dpb)}] + set ab_lowest_dp [expr {min($dpa,$dpb)}] + set ab_diff_dp [expr {$dpb - $dpa}] + + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + #puts stdout "sc:$sc - (dpb:$dpb - dpa:$dpa) = $sc - [expr {$dpb - $dpa}]. dp result: $dpr" + + set desired_min_dp [expr {min($dpdesired,$ab_highest_dp)}] + + #sc must not be < $dpa, or the calculation of sr1 below will not be a pure integer operation! + + #!todo - sc should be large enough that dpdesired+2 dp of digits always available for accurate/consistent rounding. + # (so we can differentiate betw .50 and .5x when using a rounding method for which this makes a difference) + + if {$dpr != ($dpdesired + 2)} { + incr sc [expr {max((-$sc + $dpa),$ab_diff_dp)}] + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + + if {$dpr < $desired_min_dp} { + incr sc [expr {$desired_min_dp - $dpr}] + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + } + } + #puts stdout "sc:$sc - ($dpb - $dpa) = $sc - [expr {$dpb - $dpa}]. dp result: $dpr" + + #puts stdout "calc: $sa * (10**$sc) / $sb" + set sr1 [expr {$sa * (10**$sc) / $sb}] ;#integer maths. ($sc always positive) Result will not be in scientific notation, and will not have a radix point. + if {$sr1 eq 0} { + set sr 0 + set er 0 + } else { + #jmn + #round if dpdesired < dpr + #(this result may be used as an intermediate for other calcs - not appropriate to aggressively round or limit sigfigs unless explicitly set using dpdesired) + if {$dpdesired >= $dpr} { + set sr $sr1 + set er [expr {$ea - $eb - $sc}] + } else { + set excess [expr {$dpr - $dpdesired}] + set sr [string range $sr1 0 end-$excess] + + set tail [string range $sr1 end-[expr {$excess -1}] end-[expr {$excess -2}]] + lassign [split $tail {}] e1 e2 + #puts stdout "dpr: $dpr len sr1:[string length $sr1] sr1: $sr1 sr: $sr tail: $tail" + + if {$e1 >= 6} { + incr sr + } elseif {$e1 == 5} { + if {$e2 == 0} { + #round based on current rounding method. + set sr [do_round $sr $e1 $e2] + + #round half up + #incr sr + } else { + #>50 always round up. + incr sr + } + } + set er [expr {$ea - $eb - $sc +$excess}] + } + } + return [list decimal $sr $er] + } + + + #-------------------------------------------- + #!todo - something. + # - what about countries which use USD or AUD etc instead of having their own currency? + variable currency_symbol + variable currency_country + + #e.g http://coinmill.com/sources.html + #we use smallestmajor for their term "Smallest Currency Unit" + #This is potentially useful to implement 'cash' rounding + #- but it should be noted that this is changeable as countries withdraw their smallest units again and again whilst they inflate away savings. + + dict set currency_symbol AUD [list domain Australia majorunit dollar minorunit cent smallestmajor 0.05 symbol AUD] + dict set currency_symbol AFN [list domain Afghanistan majorunit afghani minorunit "" smallestmajor 1 symbol AFN] + dict set currency_symbol EUR [list domain "European Union" majorunit euro minorunit cent smallestmajor 0.01 symbol EUR] + dict set currency_symbol GBP [list domain "UK" majorunit pound minorunit cent smallestmajor 0.01 symbol GBP] + dict set currency_symbol NZD [list domain "New Zealand" majorunit dollar minorunit cent smallestmajor 0.10 symbol NZD] + dict set currency_symbol USD [list domain "United States" majorunit dollar minorunit cent smallestmajor 0.01 symbol USD] + dict set currency_symbol {L$} [list domain "Linden" majorunit dollar minorunit cent smallestmajor 0.01 symbol {L$} note "Second Life"] + + #For cryptocurrencies - smallestmajor could be considered to be the smallest divisibility - but there are often network limits + #on what is in practice the smallest transferable amount. (e.g transaction fees, variable based on network capacity/demand and relative value of the currency) + dict set currency_symbol BTC [list domain "Bitcoin" majorunit BTC minorunit satoshi smallestmajor 0.00000001 symbol "\U20BF"] + dict set currency_symbol BCH [list domain "Bitcoin Cash" majorunit BCH minorunit satoshi smallestmajor 0.00000001 symbol "BCH"] + #eth smallest major 1e-18 'wei' - but 'gwei' 1e-9 is commonly uses as 'Ethereum gas' is paid in gwei + dict set currency_symbol ETH [list domain "Ethereum" majorunit ETH minorunit wei smallestmajor 0.000000000000000001 symbol "ETH"] + #-------------------------------------------- + + + + + #shortcut methods - from dollars to various fractional-cent units. + #interp alias {} ${ns}::d2c {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-0 {*}$args}}] + interp alias {} ${ns}::d2c {} ::dollarcent::convert d to c-0 + #interp alias {} ${ns}::d2xc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-1 {*}$args}}] + interp alias {} ${ns}::d2xc {} ::dollarcent::convert d to c-1 + #interp alias {} ${ns}::d2cc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-2 {*}$args}}] + interp alias {} ${ns}::d2cc {} ::dollarcent::convert d to c-2 + #interp alias {} ${ns}::d2mc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-3 {*}$args}}] + interp alias {} ${ns}::d2mc {} ::dollarcent::convert d to c-3 + #interp alias {} ${ns}::d2_xc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-4 {*}$args}}] + interp alias {} ${ns}::d2_xc {} ::dollarcent::convert d to c-4 + #interp alias {} ${ns}::d2_cc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-5 {*}$args}}] + interp alias {} ${ns}::d2_cc {} ::dollarcent::convert d to c-5 + #interp alias {} ${ns}::d2_mc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-6 {*}$args}}] + interp alias {} ${ns}::d2_mc {} ::dollarcent::convert do to c-6 + namespace export d2c d2xc d2cc d2mc d2_xc d2_cc d2_mc + + #shortcut methods - from various fractional-cent units to dollars. + #interp alias {} ${ns}::c2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-0 to d {*}$args}}] + interp alias {} ${ns}::c2d {} ::dollarcent::convert c-0 to d + #interp alias {} ${ns}::xc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-1 to d {*}$args}}] + interp alias {} ${ns}::xc2d {} ::dollarcent::convert c-1 to d + #interp alias {} ${ns}::cc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-2 to d {*}$args}}] + interp alias {} ${ns}::cc2d {} ::dollarcent::convert c-2 to d + #interp alias {} ${ns}::mc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-3 to d {*}$args}}] + interp alias {} ${ns}::mc2d {} ::dollarcent::convert c-3 to d + #interp alias {} ${ns}::_xc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-4 to d {*}$args}}] + interp alias {} ${ns}::_xc2d {} ::dollarcent::convert c-4 to d + #interp alias {} ${ns}::_cc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-5 to d {*}$args}}] + interp alias {} ${ns}::_cc2d {} ::dollarcent::convert c-5 to d + #interp alias {} ${ns}::_mc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-6 to d {*}$args}}] + interp alias {} ${ns}::_mc2d {} ::dollarcent::convert c-6 to d + namespace export c2d xc2d cc2d mc2d _xc2d _cc2d _mc2d + namespace export convert roundcents + + #namespace export root + #namespace export decadd decsub decmul decdiv decpow str2dec dec2str sigfigs + #namespace export roundcents multiply_as_decimal divide_as_decimal add_as_decimal sub_as_decimal power_as_decimal + + + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace dollarcent ---}] +} + +#+ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval dollarcent::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace dollarcent::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace dollarcent::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval dollarcent::system { + #*** !doctools + #[subsection {Namespace dollarcent::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide dollarcent [namespace eval dollarcent { + variable pkg dollarcent + variable version + set version 1.1 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/vendormodules/include_modules.config b/src/vendormodules/include_modules.config index a9c143af..f46ecd5d 100644 --- a/src/vendormodules/include_modules.config +++ b/src/vendormodules/include_modules.config @@ -13,6 +13,7 @@ set local_modules [list\ c:/repo/jn/tclmodules/tomlish/modules tomlish\ c:/repo/jn/tclmodules/tomlish/modules test::tomlish\ c:/repo/jn/tclmodules/dictn/modules dictn\ + c:/repo/jn/tclmodules/dollarcent/modules dollarcent\ ] set fossil_modules [dict create\ diff --git a/src/vfs/_vfscommon.vfs/modules/dollarcent-1.1.tm b/src/vfs/_vfscommon.vfs/modules/dollarcent-1.1.tm new file mode 100644 index 00000000..4c69c854 --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/dollarcent-1.1.tm @@ -0,0 +1,1522 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2025 +# +# @@ Meta Begin +# Application dollarcent 1.1 +# Meta platform tcl +# Meta license MIT +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin dollarcent_module_dollarcent 0 1.1] +#[copyright "2025"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require dollarcent] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of dollarcent +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by dollarcent +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6-}] + + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +namespace eval dollarcent { + #only export main api - see at end of namespace + #todo - tidy up functions in main namespace and move some to lib etc + #namespace export {[a-z]*}; # Convention: export all lowercase + #variable xyz + + #*** !doctools + #[subsection {Namespace dollarcent}] + #[para] Core API functions for dollarcent + #[list_begin definitions] + + variable strict 1 ;#0|1|2 + # 0 = ignore intermixing of conversions with different fractional-cent units. + # 1 = warn with msg to stderr about intermixing + # 2 = raise error + + #2025 - todo + #WARNING - statefulness of variables such as strict,units,rounding_method - suggest we should use an object-like interface - otherwise concurrent use in different contexts is risky. + + variable units "" ;#last active units used for fractional-cents. Raise error or warning if attempt to use a different conversion unit without explicitly resetting. + + proc units {{fractional_cent_unit "-"}} { + variable units + if {$fractional_cent_unit eq "-"} { + return $units + } else { + if {$fractional_cent_unit eq ""} { + #allow 'reset' so that whatever next unit is used in conversion will become next units value. + set units "" + } else { + foreach {a b} {c c-0 xc c-1 cc c-2 mc c-3 _xc c-4 _cc c-5 _mc c-6} { + if {$fractional_cent_unit eq $a} { + set fractional_cent_unit $b + break + } + } + if {![string match "c-*" $fractional_cent_unit]} { + error "Expected fractional cent unit such as 'c' 'xc' 'mc' '_xc' or 'c-1' 'c-2' 'c-3' 'c-4' etc - got: '$fractional_cent_unit'" + } + set units $fractional_cent_unit + } + } + } + + variable rounding_methods + + set rounding_methods [dict create] + dict set rounding_methods halfeven {unbiased statisticians bankers convergent dutch gaussian} + dict set rounding_methods halfup {up euro} + dict set rounding_methods halfdown {down} + dict set rounding_methods halfawayfromzero {} + dict set rounding_methods halftowardszero {} + dict set rounding_methods unsupported {stochastic swedish cash} + #'cash' method (sometimes called swedish) depends on smallest unit of currency in play for a particular jurisdication/country + + variable rounding_method halfup ;#default rounding 0.5 upwards + + proc roundingmethods {} { + variable rounding_methods + return $rounding_methods + } + + #--------------------------------- + #rounding implementations + + proc do_round_halfeven {num e1 e2} { + if {($e1 eq "5") && ($e2 eq "0")} { + set last [string range $num end end] + if {($last % 2) == 0} { + #even + incr num + } + } elseif {$e1 eq "5"} { + incr num + } + return $num + } + + proc do_round_halfup {num e1 e2} { + if {$e1 eq "5"} { + incr num + } + return $num + } + proc do_round_halfdown {num e1 e2} { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + # ${num}5x is > ${num}50 round up. + incr num + } + #exactly ${num}50 - leave num as is. (ie round down) + } + return $num + } + proc do_round_halfawayfromzero {num e1 e2} { + if {$num > 0} { + set num [do_round_halfup $num $e1 $e2] + } else { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + incr num -1 + } + } + } + return $num + } + proc do_round_halftowardszero {num e1 e2} { + if {$num > 0} { + if {$e1 eq "5"} { + if {$e2 ne "0"} { + incr num -1 + } + } + } else { + set num [do_round_halfup $num $e1 $e2] + } + return $num + } + #---------------------------------- + proc do_round {num e1 e2} { + variable rounding_method + variable rounding_methods + set meth "-" + if {$rounding_method ni [dict keys $rounding_methods]} { + foreach m [dict keys $rounding_methods] { + set aliases [dict get $rounding_methods $m] + if {$rounding_method in $aliases} { + if {"::dollarcent::do_round_$m" in [info commands ::dollarcent::*]} { + set meth $m + } + break + } + } + } else { + set meth $rounding_method + } + if {$meth eq "-"} { + error "rounding method '$rounding_method' not supported" + } + + do_round_$meth $num $e1 $e2 + } + + #2010-04 - roundingmethod not yet in use. review. Get the basic roundhalfup version working and fully tested first! + proc roundingmethod {args} { + variable rounding_method + variable rounding_methods + if {![llength $args]} { + return $rounding_method + } else { + set allmethods [list] + foreach m [dict keys $rounding_methods] { + if {$m ne "unsupported"} { + lappend allmethods $m + lappend allmethods {*}[dict get $rounding_methods $m] + } + } + + set desiredmethod [lindex $args 0] + if {$desiredmethod ni $allmethods} { + error "rounding method '$desiredmethod' is not supported" + } + set rounding_method $desiredmethod + } + } + + + set ns [namespace current] + + + #proc convert {amount source TO target args} {} ;#old v 1.0 signature - not easy to alias or pipeline + package require punk::args + punk::args::define { + @id -id ::dollarcent::convert + @cmd -name dollarcent::convert -help\ + "Convert between a floating point number representing a value specified in the + major unit of a currency - to an integer representing the number of fractional + parts of the minor unit. + e.g if dealing with dollars and cents and using c-level cc (c-2) + convert between a dollar figure like 1.00 + and a quantity of fractional-cents 10000" + @form -form dollar + @leaders -min 3 -max 3 + double_amount_type -type string -choices {dollars} -help\ + "'dollars' means convert FROM the large unit of whatever currency is + being processed" + to -type literal(TO)|literal(to) + target_type -type string -choicerestricted 0 -choices {c xc cc mc _xc _cc _mc}\ + -choicelabels { + "c"\ + "equiv: c-0" + "xc"\ + "equiv: c-1" + "cc"\ + "equiv: c-2" + "mc"\ + "equiv: c-3" + "_xc"\ + "equiv: c-4" + "_cc"\ + "equiv: c-5" + "_mc"\ + "equiv: c-6" + } -help\ + "The scale of the small unit of currency for calculation purposes. + e.g c (c-0) is 1 major unit to 100 + mc (c-3) is 1 major unit to 100_000 + _mc (c-6) is 1 major unit to 100_000_000 + + c happens to correspond to USD $1 to 100c + and _mc happens to correspond to 1BTC to 100Million Satoshi, + but generally you'd want a higher level than the number of + small units per major unit so that rounding errors are reduced + to well below the level of the minor currency unit. + " + @opts + -strict -type integer -default 1 -choices {0 1 2}\ + -choicelabels { + 0 {Silence even if units (c-level) used don't match last used} + 1 {Warn on stderr if units have changed} + 2 {Error if units have changed} + } -choicecolumns 1 + @values -min 1 -max 1 + amount -type double -help\ + "Quantity of the largest units of the currency (a conceptual 'Dollar' amount). + For another currency this might be Euros or BTC. + It is specified as a floating-point number with at least 2 decimal places, + but the maximum allowed decimal places is determined by the target_type." + + + @form -form cent + @leaders -min 3 -max 3 + integer_amount_type -type string -choicerestricted 0 -choices {c xc cc mc _xc _cc _mc}\ + -choicelabels { + "c"\ + "equiv: c-0" + "xc"\ + "equiv: c-1" + "cc"\ + "equiv: c-2" + "mc"\ + "equiv: c-3" + "_xc"\ + "equiv: c-4" + "_cc"\ + "equiv: c-5" + "_mc"\ + "equiv: c-6" + } -help\ + "The scale of the small unit of currency for calculation purposes. + e.g c (c-0) is 1 major unit to 100 + mc (c-3) is 1 major unit to 100_000 + _mc (c-6) is 1 major unit to 100_000_000 + + c happens to correspond to USD $1 to 100c + and _mc happens to correspond to 1BTC to 100Million Satoshi, + but generally you'd want a higher level than the number of + small units per major unit so that rounding errors are reduced + to well below the level of the minor currency unit. + " + to -type literal(TO)|literal(to) + target_type -type string -choices {dollars} -help\ + "'dollars' means convert TO the large unit of whatever currency is + being processed" + + @opts + -strict -type integer -default 1 -choices {0 1 2}\ + -choicelabels { + 0 {Silence even if units (c-level) used don't match last used} + 1 {Warn on stderr if units have changed} + 2 {Error if units have changed} + } -choicecolumns 1 + @values -min 1 -max 1 + amount -type double -help\ + "Integer quantity being used for conversion to the major unit amount. + The same scale (c-level) must be used in converting to and from large + and small units for any calculations to make sense." + + + } + proc convert {args} { + set amount_type [tcl::prefix::match -error "" {dollars} [lindex $args 0]] + if {$amount_type eq "dollars"} { + set argd [punk::args::parse $args -form dollar withid ::dollarcent::convert] + } else { + set amount_type [lindex $args 0] + set argd [punk::args::parse $args -form cent withid ::dollarcent::convert] + } + + variable strict ;#whether and how to alert programmer if subsequent calls use different fractional-cent units. + variable units ;#we remember the fractional cent units used in previous calls and disallow different units unless explicitly set with dollarcent::units. + + lassign [dict values $argd] leaders opts values received + + set bestrict [dict get $opts -strict] + + set target_type [dict get $leaders target_type] + set amount [dict get $values amount] + + #c-level aliases + set c_aliases [dict create c c-0 xc c-1 cc c-2 mc c-3 _xc c-4 _cc c-5 _mc c-6] + + #puts stdout "->convert $amount $amount_type $TO $target" + if {$amount_type eq {dollars}} { + # where c-0 = cents, c-1 represents cents * 10-1 or 'tenths of a cent', c-2 = 'hundredths of a cent' etc. + #c, xc, cc, mc are shortcuts for these - designed to match the convenience functions dc2c dc2xc dc2cc dc2mc dc2_xc, c2dc xc2dc mc2dc etc + # ie roman numeralesque + # x = 10 + # c = 100 (leading char only - trailing one means cents!) + # m = 1000 + # _x = 10000 (underscore to represent 'overbar' used in roman numerals to indicate multiplication by 1000) + # _c = 100000 + # _m = 1000000 + + #dc2_xc dc2_mc + if {[dict exists $c_aliases $target_type]} { + set target_type [dict get $c_aliases $target_type] + } + + if {$units eq ""} { + #1st use in this interp - ok - just set the units. + set units $target_type + } else { + if {$target_type ne $units} { + set msg "The fractional-cent unit already in use is '$units' but the current conversion specified: '$target_type'. Call 'dollarcent::units ' if you wish to change the size of the fractional-cent in use." + append msg "\ne.g 'c-0' = cents, 'c-1' or 'xc' = tenths of a cent, 'c-2' or 'cc' = hundredths of a cent, 'c-3' or 'mc' = thousandths, 'c-4' or '_xc = 10-thousandths etc." + if {$bestrict == 1} { + puts stderr $msg + } elseif {$bestrict == 2} { + error $msg + } else { + #ignore - but *don't* reset units - this may have been a 'oneshot' use of a different unit by use of the -strict 0 option to 'convert'. + #We require that the programmer specifically set the units. + } + } + } + + + set dollars $amount + + lassign [split $target_type -] _c power + #if {$power > 20} { + # error "dollarcent package probably can't handle such small fractions of a cent. '10 to the -20' may be the limit. Check code in dollarcent module and adjust this test as necessary." + #} + + #puts stdout "power is $power" + #puts stdout "dollars: $dollars" + set format "%lld.%1d%1d[string repeat %1d $power]%s" ;#note - this is lower case ll not numeral 11! + + set vars [lrepeat [expr {$power + 2}] _c] ;#just a list of placeholder vars for the scan function to + + set partcount [scan $dollars $format bucks {*}$vars _disallowed] + set dpcount [expr {$partcount -1}] ;#subtract the LHS to get the count of number of decimal places. + if {$dpcount < 2 || ($dpcount > (2 + $power))} { + return -code error "Incorrect precision for target type '$target_type'. Dollar amount must be a decimal number with between 2 and [expr {2 + $power}] digits following the decimal point." + } + + #------------------------------- + #Note: We could do this.. but this is a floating point multiplication. + # - for $power = 0, this seems to work up to about $(2**45).34 ie 35trillion,184billion,372million,88thousand,832 dollars + # - if the chosen base unit is 10 thousandths of a cent, then rounding errors will occur at somewhere around 550billion dollars. + # - Presumably rounding errors regarding a cent for such large sums are not an issue for the vast majority of use-cases - but it's the intention that this module + # eventually be accurate and flexible enough to be used in all sorts of situations e.g for some pseudo-currency in a virtual-world/game economy or perhaps even in a hyperinflationary environment. + # (No fitness for use in such situations is implied) + #return [expr {round($dollars*100*(10**$power))}] + #------------------------------- + + + set subcents [decmul [str2dec $dollars] [str2dec [expr {100*(10**$power)}]]] + #set result [expr {round([dec2str $subcents])}] ;#don't do this - round will do funny things for large numbers. + set result [dec2str $subcents] + + lassign [split $result .] whole part + set c1 [string range $part 0 0] + if {$c1 >= 5} { + if {$c1 > 5} { + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } else { + #c1 exactly 5 + set tail [string range $part 1 end] + if {([string length $tail] ==0) || ($tail == 0)} { + #entirety of 'part' is equivalent to exactly 0.5 + ##!todo - round based on currently active roundingmethod + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } else { + #there is something non-zero folowing c1 + #therefore round up - no matter what the currently active rounding method. + if {$whole > 0} { + incr whole + } else { + incr whole -1 + } + } + } + } + return $whole + + } else { + #we're converting fractional cents to dollars & cents. + + if {[string match "-*" $amount]} { + set amount [string range $amount 1 end] + set sign - + } else { + set sign "" + } + + set int_pennyparts $amount + if {[dict exists $c_aliases $amount_type]} { + set amount_type [dict get $c_aliases $amount_type] + } + + if {$units eq ""} { + #1st use in this interp - ok - just set the units. + set units $amount_type + } else { + if {$amount_type ne $units} { + set msg "The fractional-cent (c-level) unit already in use is '$units' but the current conversion specified: '$amount_type'. Call 'dollarcent::units ' if you wish to change the size of the fractional-cent in use." + append msg "\ne.g 'c-0' = cents, 'c-1' or 'xc' = tenths of a cent, 'c-2' or 'cc' = hundredths of a cent, 'c-3' or 'mc' = thousandths, 'c-4' or '_xc = 10-thousandths etc." + if {$bestrict == 1} { + puts stderr $msg + } elseif {$bestrict == 2} { + error $msg + } else { + #ignore - but *don't* reset units - this may have been a 'oneshot' use of a different unit by use of the -strict 0 option to 'convert'. + #We require that the programmer specifically set the units. + } + } + } + + + + lassign [split $amount_type -] _c power + if {$power > 19} { + error "dollarcent package probably can't handle such small fractions of a cent. '10 to the -19' may be the limit. Check code in dollarcent module and adjust this test as necessary." + } + + + if {$target_type ne {dollars}} { + error "For conversion amount_type '$amount_type' - unknown target type '$target_type'" + } + #format string note - this is lowercase ll not number 11! + if {[scan $int_pennyparts %lld%c int_pennyparts _disallowed] == 1} { + #---------------- + #NOTE! 'format %.2f' does implicit rounding - and also varies in behaviour across platforms. This is not suitable for use here. + #return [format %.2f [expr {$int_pennyparts /(100.0 *(10**$power))}]] ;#NOTE - %.2f rounds in a strange manner - and is inconsistent across tcl platforms! + #----------------- + + #----------------- + #return [expr {$int_pennyparts /(100.0 *(10**$power))}] + #----------------- + + set r [decdiv [str2dec $int_pennyparts] [str2dec [expr {100*(10**$power)}]]] + #puts stdout "r -> $r" + + lassign $r _dec significand exp + + #trailing zeroes not useful here + #(normally trailing zeroes are sigfigs) + set r_significand [string reverse $significand] + + set exp2 $exp + foreach z [split $r_significand ""] { + if {$z eq "0"} { + incr exp2 + } else { + break + } + } + set significand2 [string trimright $significand "0"] + + #return [dec2str [list decimal $significand $exp]] ;#dollar result + set result [dec2str [list decimal $significand2 $exp2]] ;#dollar result + + if {[string first . $result] < 0} { + set result $result.00 + } else { + lassign [split $result .] dollarpart centpart + if {[string length $centpart] == 1} { + set centpart ${centpart}0 + } + if {[string length $dollarpart] > 1} { + set dollarpart [string trimleft $dollarpart 0] + } + set result $dollarpart.$centpart + } + return $sign$result + } else { + return -code error "Only an integer number of units can be converted to dollars and cents. Please round before converting. amount:'$int_pennyparts'" + } + } + } + + proc roundcents {dollars} { + lassign [split $dollars .] bucks cents + if {[string length $cents] < 2} { + return -cdoe error "'roundcents' requires that dollar amount be given with at least 2 decimal places" + } + + set decimal_dollaramount [str2dec $dollars] + set decimal_100 [str2dec 100] + set decimal_cents [decmul $decimal_dollaramount $decimal_100] + + set fpcents [dec2str $decimal_cents] + #puts stdout "fpcents-> $fpcents" + + lassign [split $fpcents .] wholecents frac + set frac1 [string range $frac 0 0] + set fractail [string range $frac 1 end] + if {$frac1 >= 5} { + if {$frac1 == 5} { + if {([string length $fractail] == 0) || ($fractail == 0)} { + #entirety of 'frac' is equivalent to .5 + #use active rounding method to round + #!todo - roundingmethods + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } else { + #the x in .5x is non-zero - always round up no matter what rounding method is in effect. + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } + } else { + if {$wholecents > 0} { + incr wholecents + } else { + incr wholecents -1 + } + } + } + + #set cents [expr {round($fpcents)}] + + set d [c2d -strict 0 $wholecents] + return $d + } + + #normalize a string representing dollars and cents, from a US,AU,NZ etc perspective + #ie doesn't cater for european conventions of comma as decimal separator. + #This normalization from the perspective of this library: + # conversion from other conventions such as European should be done at input/output of whole process. (using locale etc) + + # e.g '$2.1' -> '2.10' + #Must also cope with underscores and commas in combination with leading zeros + #Must not lose decimal places e.g $10.33335 -> 10.33335 + #But we convert both 10. and 10.0 to 10.00 (not correct from a sigfig perspective - but more useful) + proc normaldollars {dollaramount} { + set amount [string trim $dollaramount] + set amount [string map {{-$} {$-}} $amount] ;# -$2.10 equivalent to $-2.10 + if {[string index $amount 0] eq {$}} { + } + set amount [string trimleft $amount {$}] + if {$amount in {"" "-"}} { + error "'$dollaramount' is not in a format that normaldollars can recognize" + } + + if {[string match "-*" $amount]} { + set amount [string range $amount 1 end] + set sign - + } else { + set sign "" + } + if {[string match ".*" $amount]} { + set amount 0$amount + } + if {[string match "*." $amount]} { + set amount ${amount}00 + } + + if {[string first . $amount] < 0} { + set amount $amount.00 + } else { + lassign [split $amount .] dollarpart centpart + if {[string length $centpart] == 1} { + set centpart ${centpart}0 + } + if {[string length $dollarpart] > 1} { + set dollarpart [string trimleft $dollarpart 0] + } + set amount $dollarpart.$centpart + } + + return $sign$amount + } + + + variable scale 38 ;#fairly arbitrary - but 28 supposedly matches that of Python. 'ISO COBOL 2002 standard' uses 32, Transact SQL has a max of 38. + variable maxmantisse [expr {10**$scale}] + variable bndmantisse [expr {10*$maxmantisse}] + proc set_scale {newscale} { + variable scale + variable maxmantisse + variable bndmantisse + + set scale $newscale + set maxmantisse [expr {10**$scale}] + set bndmantisse [expr {10*$maxmantisse}] + } + + + #---------------------------------------------------------------- + # Rescale -- + # Rescale the number (using proper rounding) + # + # Arguments: + # decimal in the form {decimal significand exponent} + # + # Result: + # Rescaled number (as a list) + # + proc decrescale {decimal {newscale ""}} { + variable maxmantisse + variable bndmantisse + + lassign $decimal _decimal significand exponent + + if { abs($significand) <= $maxmantisse } { + return [list decimal $significand $exponent] + } + + set rest [expr {$significand % 10}] ;#for case where $maxmantisse < abs($significand) <= $bndmantisse + while { abs($significand) > $bndmantisse } { + set rest [expr {$significand % 10}] + set significand [expr {$significand/10}] + incr exponent + } + + + if { $rest > 5 } { + if { $significand > 0 } { + incr significand + } else { + incr significand -1 + } + } elseif { $rest == 5 } { + #halfup rounding + if { $significand > 0 } { + incr significand + } else { + incr significand -1 + } + + #bankers rounding + #if { ($significand/10) % 2 == 1 } { + # incr significand + #} + } + + return [list decimal $significand $exponent] + } + + proc str2dec {string} { + set pos [string first . $string] + if {$pos < 0} { + #significand = mantissa + set significand $string + set significand [string trimleft $string "0"] + if {$significand eq ""} { + set significand 0 + } + set exponent 0 + } else { + set fraction [string range $string $pos+1 end] + set significand [string trimleft [string map {. ""} $string] 0] + if {$significand eq ""} { + set significand 0 + } + set exponent [expr {-[string length $fraction]}] + } + return [list decimal $significand $exponent] + } + + proc dec2str {decimal_as_list} { + #puts stdout "==>dec2str $decimal_as_list" + lassign $decimal_as_list _decimal significand exponent + if {![string length $significand]} { + set significand 0 + } + if {$significand < 0} { + set sign - + } else { + set sign "" + } + set significand [string map {- ""} $significand] + if {$exponent > 0} { + if {$significand == 0} { + set string 0 + } else { + set string $sign$significand[string repeat 0 $exponent] + } + } else { + set digits [string length $significand] + + set exponent [expr {abs($exponent)}] + if {$digits >= $exponent} { + if {$exponent == 0} { + set string $sign$significand + } else { + if {$digits == $exponent} { + #e.g 10 * 10**-2 + set lhs "0" + } else { + set lhs [string range $significand 0 [expr {$digits-$exponent-1}]] + } + set string $sign$lhs.[string range $significand [expr {$digits-$exponent}] end] + } + } else { + set string ${sign}0.[string repeat 0 [expr {$exponent-$digits}]]$significand + } + } + return $string + } + + proc multiply_as_decimal {a b args} { + return [dec2str [decmul [str2dec $a] [str2dec $b] {*}$args]] + } + proc divide_as_decimal {a b args} { + return [dec2str [decdiv [str2dec $a] [str2dec $b] {*}$args]] + } + proc add_as_decimal {a b args} { + return [dec2str [decadd [str2dec $a] [str2dec $b] {*}$args]] + } + proc power_as_decimal {a b args} { + return [dec2str [decpow [str2dec $a] [str2dec $b] {*}$args]] + } + + + #round 'val' to 'dp' decimal places + proc dpround {val dp} { + error "not implemented" + } + #round 'val' to 'sigfigs' significant figures + proc sigfiground {val sigfigs} { + error "not implemented" + } + + #significant figures 'are a rather crude way of tracking precision' + # see http://scienceblogs.com/goodmath/2009/03/basics_significant_figures.php + # - NOTE - to review - are sigfigs really even relevant to currency work? + proc sigfigs {floatstr} { + + #1) all non-zero digits are considered significant + #2) zeros between non-zeros are significant. + #3) leading zeros not significant. + + # - for this function - we will assume trailing zeros in numbers with no decimal point are insignificant. + # There is an ambiguity here because it could be that the zeros are significant but the value just happens to be round.. + # we adopt the convention that a trailing . on the input will indicate that the previous zeros are significant. + # e.g "100" has 1 sigfigs but "100." has 3 sigfigs. + + #rules when performing calculations: + #a) for multiplication and division the final answer should contain only as many sigfigs as the number with the *least* number of sigfigs. + #b) for addition and subtraction - keep the number of sigfigs in the input with the smallest number of *decimal places* + + + set i 0 + set s 0 + set in_sigfigs 0 + set sigs "" ;#significant digits + set dp 0 + set in_dp 0 + if {[string first . $floatstr] >= 0} { + set has_radix 1 ;#radix is a more general term for decimal point. + } else { + set has_radix 0 + } + if {[string match "-*" $floatstr]} { + set floatstr [string map {- ""} $floatstr] + incr i + } + foreach c [split $floatstr ""] { + if {$c eq "0"} { + if {$in_sigfigs} { + append sigs "0" + } + } else { + if {$c ne "."} { + if {!$in_sigfigs} { + set in_sigfigs 1 + set s $i + } + append sigs $c + } else { + set in_dp 1 + } + } + if {$c ne "."} { + if {$in_dp} { + incr dp + } + } + incr i + } + if {!$has_radix} { + #no radix. We will consider trailing zeros to be insignificant. + set sigs [string trimright $sigs "0"] + } + + return [list count [string length $sigs] startindex $s digits $sigs dp $dp] + } + + #decimal sum + proc decadd {a b} { + lassign $a _decimal sa ea ;#significand-a (aka mantissa-a), exponent-a + lassign $b _decimal sb eb + + if {$ea > $eb} { + set sa [expr {$sa * 10 ** ($ea-$eb)}] + set er $eb + } else { + set sb [expr {$sb * 10 ** ($eb-$ea)}] + set er $ea + } + set sr [expr {$sa + $sb}] + return [list decimal $sr $er] + } + + proc decpow2 {a b} { + #a**b + lassign $a _decimal sa ea + lassign $b _decimal sb eb + if {$sb eq 0} { + return [list decimal 1 0] + } else { + if {$sb < 0} { + #a**-n = 1/a**n + error "negative exponent not supported - sorry - try plain expr" + + } else { + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + + #powx = intermediate power x + + #sa **(sb * ten_2_eb) * 10**(ea*sb*(ten_2_eb)) + #sa **(sb * ten_2_eb) * 10**(exp1) + #sa **(sb * ten_2_eb) * pow2 + #pow3 * pow2 + + puts stdout "--->sa **(sb * ten_2_eb) * 10**(ea*sb*(ten_2_eb))<---" + puts stdout "--->sa **(sb * ten_2_eb) * 10**(exp1)<---" + + puts stdout "===>[subst {($sa ** ($sb * 10 **$eb)) * (10**($ea*$sb*(10**$eb))) }]<====" + set ten_2_eb [expr {10**$eb}] + puts stdout "->ten_2_eb: $ten_2_eb" + set exp1 [expr {$ea * $sb * $ten_2_eb}] + if {$exp1 < 0 } { + set exp1plus [string map {- ""} $exp1] + set d [expr {10**$exp1plus}] + puts stdout "...10**$exp1plus= $d" + set pow2 [divide_as_decimal 1 $d] + } else { + set pow2 [expr {10**$exp1}] + } + + puts stdout "->exp1: $exp1" + puts stdout "->pow2: $pow2" + + set exp2 [expr {$sb * $ten_2_eb}] + if {$exp2 < 0} { + set exp2plus [string map {- ""} $exp2] + set d [expr {$sa**$exp2plus}] + puts stdout "...$sa**$exp2plus= $d" + set pow3 [divide_as_decimal 1 $d] + } else { + set pow3 [expr {$sa ** $exp2}] + } + + set a1 [multiply_as_decimal $pow3 $pow2] + #return $a1 + return [str2dec $a1] + + set a1 [expr {($sa ** ($sb * 10 **$eb)) * (10**($ea*$sb*(10**$eb))) }] + return [str2dec $a1] + #set er [expr {$ea + $eb}] + + return [list decimal $sr $er] + } + } + } + + #broken + proc decpow {a b} { + puts stdout "decpow $a $b" + puts stderr "REVIEW - noted as 'broken' in source'" + #a**b + #make sure that all 'x ** y' operations within 'expr' involve integer x and positive integer y. + + lassign $a _decimal sa ea + lassign $b _decimal sb eb + if {$sb eq 0} { + return [list decimal 1 0] + } else { + if {$eb < 0} { + set ebplus [expr {abs($eb)}] + #set ten2eb [dec2str [decdiv {decimal 1 0} [str2dec [expr {10**$ebplus}]] ]] + #set x [expr {$sb * $ten2eb}] + set decpow [decpow {decimal 10 0} [list decimal $ebplus 0]] + + set divscale [expr {min(2,[string length [lindex $decpow 1]])}] + puts stdout "!!! decdiv {decimal 1 0} $decpow $divscale" + set ten2eb [decdiv {decimal 1 0} $decpow -dp $divscale] + set x [decmul [list decimal $sb 0] $ten2eb] + } elseif {$eb == 0} { + set x [list decimal $sb 0] + } else { + #set x [expr {$sb * (10 **$eb)}] + set x [decmul [list decimal $sb 0] [decpow {decimal 10 0} [list decimal $eb 0]]] + } + puts stdout "sb * (10 ** eb) -> $sb * (10 ** $eb) ----> x: $x" + + #raw eqn: + # sa*(10**ea) ** sb*(10**eb) + # using rule (xy)**n = (x**n)*(y**n) + # sa**(sb * 10**eb) * (10**ea)**(sb * 10**eb) + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + #sa**(bdec) * 10**(ea*bdec) + + #puts stdout "--->sa **(sb*(10**eb)) * 10**(ea * sb*(10**eb)))<---" + if {$ea == 0} { + puts stdout "------------ ea = 0" + #$a = $sa (since 10**0 = 1) + + #sa **(sb * (10**eb)) + #exponent law: x**mn = (x**m)**n + #(sa ** sb) ** (10**eb) + if {$eb == 0} { + puts stdout "------------ eb = 0 sa: $sa sb: $sb" + return [list decimal [expr {$sa ** $sb}] 0] + } elseif {$eb > 0} { + #sa ** (sb * (10**eb)) + puts stdout "------------ eb > 0" + #set rdec [decpow [str2dec [expr {$sa ** $sb}]] [list decimal 1 $eb]] + set r [expr ($sa ** $sb) ** (10**$eb)] + set a1 [str2dec $r] + return $a1 + + #puts stdout "xxxxxxx eb: $eb" + #return [list decimal [expr {$sa ** $sb}] [expr {10 ** $eb}]] + } else { + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #sa**(sb * 10**eb) * 10**0 + #---------------------------------------------- + puts stdout "------------ eb < 0" + #b = sb * (10** eb) + #if eb negative then + #b = sb * (1/(10**abs(eb))) + #b = sb / (10**abs(eb)) + #negative exponent of number b - therefore equivalent of 1/(sb**abs(eb)) + if {$sa < 0} { + error "negative values not handled here.. sorry" + } + #exponent law: x**(m/n) = nth root of x**m + + #sb * 1/10**abs(eb) + + #set 10eb [decpow {decimal 10 0} [list decimal [expr {abs($eb)}] 0]] + set 10eb [expr {10**abs($eb)}] + set b [decdiv [list decimal $sb 0] [list decimal $10eb 0]] + + puts "zzz" + return [decpow [list decimal $sa 0] $b] + + + + + #puts stdout "about to find '10**abs($eb)'th root of ($sa * (10**$sb))" + #set a1 [str2dec [root [expr {$sa * (10**$sb)}] [expr {10**abs($eb)}]]] + #return $a1 + + #puts stdout "about to root ($sa ** $sb) 10**[expr abs($eb)]" + #set a1 [str2dec [root [expr {$sa**$sb}] [expr {10**abs($eb)}]]] + #return $a1 + } + } else { + puts stdout "nonzero ea = $ea" + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + + #puts stdout "--->sa **(x) * 10**(ea * x)<---" + + #$b < 0 whenever $sb <0 + set xstr [dec2str $x] + if {$sb < 0} { + #raising a to a negative power + + #1/sa**abs($sb * 10**eb) + set xplus [expr {abs($xstr)}] + + puts stdout "a)-->sa ** xplus => $sa ** $xplus" + #set lhs [decdiv {decimal 1 0} [str2dec [expr {$sa ** $xplus}]]] + + #--- + #work out a scale + set p10a [root $sa 10] + set p10b [root $xplus 10] + set sc [multiply_as_decimal $p10a $p10b] + lassign [split $sc .] int frac + set sc [string length $int] + #--- + + set lhs [decdiv {decimal 1 0} [decpow [list decimal $sa 0] [list decimal $xplus 0]] -dp $sc] + + } else { + #> 0 (not = 0 because we've already handled that) + puts stdout "b)" + #set lhs [expr {$sa**$x}] + #set lhs [str2dec [expr {$sa ** $x}]] + if {[string first . $xstr] < 0} { + #puts stdout "-->str2dec $sa ** $xstr" + set lhs [str2dec [expr {$sa ** $xstr}]] ;#integers + } else { + set lhs [decpow [list decimal $sa 0] $x] + } + } + puts stdout "**lhs: $lhs" + + + #---------------------------------------------- + #sa**(sb * 10**eb) * 10**(ea*sb*(10**eb)) + #---------------------------------------------- + + if {[string first . $xstr] < 0} { + set exp2 [expr {$ea * $xstr}] ;#integers + } else { + set exp2 [dec2str [decmul [list decimal $ea 0] $x]] + } + #puts stdout " ea * x = $ea * $x = $exp2" + if {$exp2 < 0} { + puts stdout "c)" + set exp2plus [expr {abs($exp2)}] + #set d [expr {10**$exp2plus}] + #puts stdout "...10**$exp2plus= $d" + #set rhs [divide_as_decimal 1 $d] + if {$exp2plus > $::dollarcent::scale} { + set sc $exp2plus + } else { + set sc $::dollarcent::scale + } + + set rhs [decdiv {decimal 1 0} [str2dec [expr {10 ** $exp2plus}]] -dp $sc] + + + } else { + puts stdout "d) 10 ** $exp2" + #set rhs [expr {10 ** $exp2}] + set rhs [str2dec [expr {10 ** $exp2}]] + } + puts stdout "**rhs: $rhs" + + set a1 [decmul $lhs $rhs] + return $a1 + #set a1 [multiply_as_decimal $lhs $rhs] + #return [str2dec $a1] + } + } + } + + proc root {num {n 2} args} { + if {[string is integer -strict $n]} { + return [nthroot $num $n {*}$args] + } else { + return [expr {$num ** (1.0/$n)}] + } + } + + + #shifting nth root algorithm. Tested against 1st 50 or so places of root 2 - seems to work. + # - not practical for large values of n. + proc nthroot {num {n 2} args} { + set default [list -dp "" -scale $::dollarcent::scale ] + set opts [dict merge $default $args] + + lassign [split $num "."] int frac + + set dpdesired [dict get $opts -dp] + if {$dpdesired eq ""} { + set dpdesired [string length $frac] + } + + set dpworking [expr {max($dpdesired,[dict get $opts -scale])}] + + if {[string length $frac] < $dpworking} { + lappend frac [string repeat "0" [expr {$dpworking -[string length $frac]}]] + } + + set overlen [expr {[string length $int] % $n}] + if {$overlen > 0} { + set padding [string repeat 0 [expr {$n - $overlen}]] + } else { + set padding "" + } + set int $padding$int ;#padded on left with enough zeros so that we can grab groups of $n digits + + set digits [split $int {}] + + + set frac_digits [split $frac {}] + + set flen [llength $frac_digits] + if {$flen > 0} { + lappend frac_digits {*}[lrepeat [expr {$flen * ($n-1)}] 0] ;#we need another $n-1 digits for every existing digit in the fractional part + + #b + #set overlen [expr {[llength $frac_digits] % $n}] + #if {$overlen > 0} { + # lappend frac_digits {*}[lrepeat [expr {$n-$overlen}] 0] + #} + } + + lappend digits {*}$frac_digits + + + + #set overlen [expr {[string length $frac] % $n}] + #if {$overlen > 0} { + # set padding [string repeat 0 [expr $n - $overlen]] + # #append frac [string repeat "0" $flen] ;#???? + #} else { + # set padding "" + #} + #set frac $frac$padding + + #n = degree of root to be extracted = (10**eb) + #x = radicand processed thus far + #y = root extracted thus far + #r = remainder + #a = next n digits of the radicand + #B = next digit of the root + + #invariant y**n + r = x + #ie. y is the largest integer less than the nth root of x and r is the remainder. + + set x 0 + set y 0 + set r 0 + + #base = 10 - base of number system we're using. + #puts stdout "intfrac: '$int$frac'" + #puts stdout $digits + set nblock "" + foreach d $digits { + append nblock $d + if {[string length $nblock] < $n} { + continue + } + #puts stdout ". $nblock ." + scan $nblock %d a + set nblock "" ;#ready to build nex one. + + #set x2 [expr {(10**$n) * $x + $a}] ;#running power (radicand processed so far) + + #find largest integer B such that (10y + B)**n <= (10**n)x + a + #---- + # for square root case (n =2) + # 100y**2 + 20yB +B**2 <= 100x + a + #---- + #B will always be less than base ie B < 10 (see http://en.wikipedia.org/wiki/Shifting_nth_root_algorithm ) + for {set B 0} {$B < 10} {incr B} { + if {(((10 * $y + $B) ** $n) - (10**$n * $y**$n)) <= (((10**$n) * $r) + $a)} { + #ok + } else { + #overshot + break + } + } + incr B -1 + set y2 [expr {(10 * $y) + $B}] + set r2 [expr {(10**$n)*$r + $a - ((10*$y + $B)**$n - (10**$n)*($y**$n))}] + + set y $y2 + set r $r2 + + #puts -nonewline $B + } + #puts stdout "" + #puts stdout "y: $y" + + #set a [expr {$y / pow(10,([llength $frac_digits]/$n))}] + #puts stdout "answer1: $a" + + #set sc [expr {[llength $frac_digits] / $n}] + #set sc $::dollarcent::scale + set sc $dpdesired + + set y [dec2str [decdiv [str2dec $y] [str2dec [expr {10**([llength $frac_digits]/$n)}] ] -dp $sc] ] + return $y + } + + + #decimal product + proc decmul {a b} { + #puts stdout "decmul $a $b" + lassign $a _decimal sa ea ;#significand-a exponent-a + lassign $b _decimal sb eb + + #integer operations - not floating point. + set sr [expr {$sa * $sb}] + set er [expr {$ea + $eb}] + + return [list decimal $sr $er] + } + + #decimal division + #rounds last digit. (automatically chooses scale large enough to provide additional 2 digits to determine rounding value) + proc decdiv {a b args} { + variable scale + set default [list -dp ""] + set opts [dict merge $default $args] + set dpdesired [dict get $opts -dp] + + if {$dpdesired ne ""} { + #set sc $dpdesired + set dp_exact 1 + } else { + set dpdesired $scale + set dp_exact 0 ;#we're allowed to trim repeat rhs zeros + } + if {$dpdesired >= $scale} { + set sc [expr {$dpdesired + 2}] ;#2 additional digits required for rounding + } else { + if {($scale - $dpdesired) == 1} { + set sc [expr {$dpdesired + 2}] ;#scale is just above dpdesired + } else { + set sc $scale + } + } + #set sc [expr {max($dpdesired,$scale)}] + + lassign $a _decimal sa ea ;#sa significand-a, ea exponent-a + lassign $b _decimal sb eb + + if {$ea >=0} { + set dpa 0 + } else { + set dpa [expr {abs($ea)}] + } + if {$eb >=0} { + set dpb 0 + } else { + set dpb [expr {abs($eb)}] + } + + + set ab_highest_dp [expr {max($dpa,$dpb)}] + set ab_lowest_dp [expr {min($dpa,$dpb)}] + set ab_diff_dp [expr {$dpb - $dpa}] + + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + #puts stdout "sc:$sc - (dpb:$dpb - dpa:$dpa) = $sc - [expr {$dpb - $dpa}]. dp result: $dpr" + + set desired_min_dp [expr {min($dpdesired,$ab_highest_dp)}] + + #sc must not be < $dpa, or the calculation of sr1 below will not be a pure integer operation! + + #!todo - sc should be large enough that dpdesired+2 dp of digits always available for accurate/consistent rounding. + # (so we can differentiate betw .50 and .5x when using a rounding method for which this makes a difference) + + if {$dpr != ($dpdesired + 2)} { + incr sc [expr {max((-$sc + $dpa),$ab_diff_dp)}] + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + + if {$dpr < $desired_min_dp} { + incr sc [expr {$desired_min_dp - $dpr}] + set dpr [expr {$sc - $ab_diff_dp}] ;#decimal places in the result. + } + } + #puts stdout "sc:$sc - ($dpb - $dpa) = $sc - [expr {$dpb - $dpa}]. dp result: $dpr" + + #puts stdout "calc: $sa * (10**$sc) / $sb" + set sr1 [expr {$sa * (10**$sc) / $sb}] ;#integer maths. ($sc always positive) Result will not be in scientific notation, and will not have a radix point. + if {$sr1 eq 0} { + set sr 0 + set er 0 + } else { + #jmn + #round if dpdesired < dpr + #(this result may be used as an intermediate for other calcs - not appropriate to aggressively round or limit sigfigs unless explicitly set using dpdesired) + if {$dpdesired >= $dpr} { + set sr $sr1 + set er [expr {$ea - $eb - $sc}] + } else { + set excess [expr {$dpr - $dpdesired}] + set sr [string range $sr1 0 end-$excess] + + set tail [string range $sr1 end-[expr {$excess -1}] end-[expr {$excess -2}]] + lassign [split $tail {}] e1 e2 + #puts stdout "dpr: $dpr len sr1:[string length $sr1] sr1: $sr1 sr: $sr tail: $tail" + + if {$e1 >= 6} { + incr sr + } elseif {$e1 == 5} { + if {$e2 == 0} { + #round based on current rounding method. + set sr [do_round $sr $e1 $e2] + + #round half up + #incr sr + } else { + #>50 always round up. + incr sr + } + } + set er [expr {$ea - $eb - $sc +$excess}] + } + } + return [list decimal $sr $er] + } + + + #-------------------------------------------- + #!todo - something. + # - what about countries which use USD or AUD etc instead of having their own currency? + variable currency_symbol + variable currency_country + + #e.g http://coinmill.com/sources.html + #we use smallestmajor for their term "Smallest Currency Unit" + #This is potentially useful to implement 'cash' rounding + #- but it should be noted that this is changeable as countries withdraw their smallest units again and again whilst they inflate away savings. + + dict set currency_symbol AUD [list domain Australia majorunit dollar minorunit cent smallestmajor 0.05 symbol AUD] + dict set currency_symbol AFN [list domain Afghanistan majorunit afghani minorunit "" smallestmajor 1 symbol AFN] + dict set currency_symbol EUR [list domain "European Union" majorunit euro minorunit cent smallestmajor 0.01 symbol EUR] + dict set currency_symbol GBP [list domain "UK" majorunit pound minorunit cent smallestmajor 0.01 symbol GBP] + dict set currency_symbol NZD [list domain "New Zealand" majorunit dollar minorunit cent smallestmajor 0.10 symbol NZD] + dict set currency_symbol USD [list domain "United States" majorunit dollar minorunit cent smallestmajor 0.01 symbol USD] + dict set currency_symbol {L$} [list domain "Linden" majorunit dollar minorunit cent smallestmajor 0.01 symbol {L$} note "Second Life"] + + #For cryptocurrencies - smallestmajor could be considered to be the smallest divisibility - but there are often network limits + #on what is in practice the smallest transferable amount. (e.g transaction fees, variable based on network capacity/demand and relative value of the currency) + dict set currency_symbol BTC [list domain "Bitcoin" majorunit BTC minorunit satoshi smallestmajor 0.00000001 symbol "\U20BF"] + dict set currency_symbol BCH [list domain "Bitcoin Cash" majorunit BCH minorunit satoshi smallestmajor 0.00000001 symbol "BCH"] + #eth smallest major 1e-18 'wei' - but 'gwei' 1e-9 is commonly uses as 'Ethereum gas' is paid in gwei + dict set currency_symbol ETH [list domain "Ethereum" majorunit ETH minorunit wei smallestmajor 0.000000000000000001 symbol "ETH"] + #-------------------------------------------- + + + + + #shortcut methods - from dollars to various fractional-cent units. + #interp alias {} ${ns}::d2c {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-0 {*}$args}}] + interp alias {} ${ns}::d2c {} ::dollarcent::convert d to c-0 + #interp alias {} ${ns}::d2xc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-1 {*}$args}}] + interp alias {} ${ns}::d2xc {} ::dollarcent::convert d to c-1 + #interp alias {} ${ns}::d2cc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-2 {*}$args}}] + interp alias {} ${ns}::d2cc {} ::dollarcent::convert d to c-2 + #interp alias {} ${ns}::d2mc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-3 {*}$args}}] + interp alias {} ${ns}::d2mc {} ::dollarcent::convert d to c-3 + #interp alias {} ${ns}::d2_xc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-4 {*}$args}}] + interp alias {} ${ns}::d2_xc {} ::dollarcent::convert d to c-4 + #interp alias {} ${ns}::d2_cc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-5 {*}$args}}] + interp alias {} ${ns}::d2_cc {} ::dollarcent::convert d to c-5 + #interp alias {} ${ns}::d2_mc {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt d to c-6 {*}$args}}] + interp alias {} ${ns}::d2_mc {} ::dollarcent::convert do to c-6 + namespace export d2c d2xc d2cc d2mc d2_xc d2_cc d2_mc + + #shortcut methods - from various fractional-cent units to dollars. + #interp alias {} ${ns}::c2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-0 to d {*}$args}}] + interp alias {} ${ns}::c2d {} ::dollarcent::convert c-0 to d + #interp alias {} ${ns}::xc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-1 to d {*}$args}}] + interp alias {} ${ns}::xc2d {} ::dollarcent::convert c-1 to d + #interp alias {} ${ns}::cc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-2 to d {*}$args}}] + interp alias {} ${ns}::cc2d {} ::dollarcent::convert c-2 to d + #interp alias {} ${ns}::mc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-3 to d {*}$args}}] + interp alias {} ${ns}::mc2d {} ::dollarcent::convert c-3 to d + #interp alias {} ${ns}::_xc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-4 to d {*}$args}}] + interp alias {} ${ns}::_xc2d {} ::dollarcent::convert c-4 to d + #interp alias {} ${ns}::_cc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-5 to d {*}$args}}] + interp alias {} ${ns}::_cc2d {} ::dollarcent::convert c-5 to d + #interp alias {} ${ns}::_mc2d {} apply [string map [list %ns% $ns] {{amt args} {%ns%::convert $amt c-6 to d {*}$args}}] + interp alias {} ${ns}::_mc2d {} ::dollarcent::convert c-6 to d + namespace export c2d xc2d cc2d mc2d _xc2d _cc2d _mc2d + namespace export convert roundcents + + #namespace export root + #namespace export decadd decsub decmul decdiv decpow str2dec dec2str sigfigs + #namespace export roundcents multiply_as_decimal divide_as_decimal add_as_decimal sub_as_decimal power_as_decimal + + + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace dollarcent ---}] +} + +#+ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval dollarcent::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace dollarcent::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace dollarcent::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval dollarcent::system { + #*** !doctools + #[subsection {Namespace dollarcent::system}] + #[para] Internal functions that are not part of the API + + + +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide dollarcent [namespace eval dollarcent { + variable pkg dollarcent + variable version + set version 1.1 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm index 4f108187..8384197a 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm @@ -433,20 +433,26 @@ namespace eval punk::mix::commandset::project { #scan all files in template # #TODO - deck command to substitute templates? - set templatefiles [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] + set templateinfo_list [punk::mix::commandset::layout::lib::layout_scan_for_template_files $opt_layout] set stripprefix [file normalize $layout_path] set tagmap [list [lib::template_tag project] $projectname] - if {[llength $templatefiles]} { + if {[llength $templateinfo_list]} { puts stdout "Filling template file placeholders with the following tag map:" foreach {placeholder value} $tagmap { puts stdout " $placeholder -> $value" } } - foreach templatefullpath $templatefiles { + foreach templateinfo $templateinfo_list { + lassign $templateinfo templatefullpath template_tagnames_found set templatetail [punk::repo::path_strip_alreadynormalized_prefixdepth $templatefullpath $stripprefix] - set fpath [file join $projectdir $templatetail] + foreach t $template_tagnames_found { + if {"%$t%" ni [dict keys $tagmap]} { + puts stderr "warning: No substitution available for tag: %$t% in $fpath" + } + } + if {[file exists $fpath]} { set fd [open $fpath r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data2 [string map $tagmap $data] @@ -458,7 +464,6 @@ namespace eval punk::mix::commandset::project { puts stderr "warning: Missing template file $fpath" } } - #todo - tag substitutions in src/doc tree ::cd $projectdir