It is currently April 24th, 2024, 6:28 pm

Suggestion: String Meter enhancement - Redux

Report bugs with the Rainmeter application and suggest features.
User avatar
jsmorley
Developer
Posts: 22629
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Re: Suggestion: String Meter enhancement - Redux

Post by jsmorley »

SilverAzide wrote: OK! If I did that, does that mean I could have a single Script measure that can be called from a bunch of different measures' OnChangeActions? If so, that would be great. The way it is now, I've got a ton of script measures (one for each String meter), so eliminating all that overhead would be great.
Exactly...

Sent from my Kindle Fire HDX
FlyingHyrax
Posts: 232
Joined: July 1st, 2011, 1:32 am
Location: US

Re: Suggestion: String Meter enhancement - Redux

Post by FlyingHyrax »

jsmorley wrote:So you knocked that out after a few minutes of looking at the Rainmeter Lua docs, my crappy little tutorial skin, and presumably the online Lua docs / tutorials as a reference?

Note to self: DO NOT EVER humiliate yourself by arguing with this guy about anything having to do with programming.

...

Anyway, nice work.
Um... ditto. For the record, I tried to work on this problem in Lua for an hour or two after reading the original feature suggestion thread. Lua's string.format doesn't work quite like C/Java's printf (nicer for some things, less flexible for others?) so I started trying to count digits and such "manually" and it got out of hand with floating-point rounding errors when I tried to do it numerically and issues with Lua representing large numbers with scientific notation when I tried with strings. Maybe could've figured it out eventually if I had more time but it's finals week (woohoo!)

That's a really tight solution. :thumbup: I don't like while (true) loops so much, but that's nitpicking. Even the documentation is good. Thank you for deflating my college programmer ego (it needed it). ;-)


More on topic - wouldn't adding generic number formatting to the String measure be pretty straightforward? It seems like you could basically just toss the value through C++ sprintf and propagate any formatting errors to the log.
Flying Hyrax on DeviantArt
User avatar
SilverAzide
Rainmeter Sage
Posts: 2604
Joined: March 23rd, 2015, 5:26 pm

Re: Suggestion: String Meter enhancement - Redux

Post by SilverAzide »

Hello!
I tweaked the script to make the function callable via a CommandMeasure bang, and made changes to my skin(s) to use it via OnUpdateAction. Works great! But...

I know this is discussed elsewhere, and that this is the wrong forum to ask, but I can't find a solution that I know I've seen somewhere.

Some measures, specifically ones like FreeDiskSpace where you get the total disk size, fires once during initialization and -- since the disk's total capacity never changes after that -- will never fire the OnUpdateAction. It would be nice if there was an "OnInitAction", but since there isn't, how can I force a single call to the script function when the measure is initialized?

This issue also crops up anytime a measure is measuring something that is zero (e.g., say like if a network interface is idle).


BTW, I inadvertently flipped my use of the terms "precision" and "scale". I have corrected this in the docs in the scripts. If anyone finds these scripts useful, here they are.

This script can be used when you want to bind a measure to a script measure for formatting:

Code: Select all

-- FixedPrecisionFormat v1.0 by SilverAzide
--
-- This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License.
--
----------------------------------------------------------------------------------------------------
--
-- Rainmeter's String meter formats numbers using a "variable precision, fixed scale" methodology.
-- ("Precision" is the total number of digits, not including signs and decimals, and "scale" is the
-- number of digits after the decimal.)
--
--   Examples:  3.141      (precision=4, scale=3; i.e., "NumOfDecimals=3")
--              314.159    (precision=6, scale=3)
--              3141.593   (precision=7, scale=3)
--              314159.265 (precision=9, scale=3)
--
-- This style of formatting means that the total number of digits in the resulting string, and thus
-- the string's length, is somewhat unpredictable.  Designers must include extra whitespace after
-- the numbers to account for large values, so if additional design elements follow the meter, there
-- is a possibility that the data can overflow one meter into another.  Also, when using this in
-- meters with rapidly changing values, the meter tends to show numbers that bounce around, as large
-- and small values alternate.
--
-- This script's FormatNumber function uses a "fixed precision, variable scale" methodology to
-- format numbers.  This results in values that have a more predicable string length and can lead to
-- a cleaner looking display.
--
--   Examples:  3.14159   (precision=6, scale=5)
--              314.159   (precision=6, scale=3)
--              3141.53   (precision=6, scale=2)
--              314159    (precision=6, scale=0)
--
-- This function implies use of "AutoScale" (i.e., all values will be scaled).  If scaling is not
-- desired, use the existing Rainmeter String meter instead.  Note that in theory negative scales
-- are valid; e.g., 31415900  (precision=6, scale=-2).  However, because of autoscaling, this is of
-- little value and is not supported.
--
-- REMEMBER:
--
--   This measure returns a formatted STRING.  Numeric formatting options in String meters will
--   not have any effect (NumOfDecimals, Scale, AutoScale, etc.) and should not be used.
--
-- Options
--
--   MeasureName
--
--     The name of the measure to be bound to this one (i.e., the source of the input value).
--
--   Precision
--
--     Specifies the numeric precision (i.e., the total number of digits in the number).  The
--     default is 3.
--
--   Factor
--
--     Specifies the scale factor.  Same as the Rainmeter String meter's "AutoScale" option:
--
--       1:  Scales by 1024 (default).
--       1k: Scales by 1024 with kilo as the lowest unit.
--       2:  Scales by 1000.
--       2k: Scales by 1000 with kilo as the lowest unit.
--
--     NOTE:
--
--       The value returned by the plugin includes a space between the scaled number and the scale
--       unit abbreviation.  To remove this space simply add Substitute=" ":"" to the measure.
--
--
-- Example skin:
--
-- ; measure network inbound bytes/sec
-- [MeasureNetIn]
-- Measure=NetIn
-- Interface=0
--
-- ; measure to format inbound bytes/sec; e.g., "12.34 M"
-- [FormatNetIn]
-- Measure=Script
-- ScriptFile=#@#FixedPrecisionFormat.lua
-- MeasureName=MeasureNetIn
-- Precision=4
-- Factor=1k
--
-- ; display inbound bytes/sec; e.g., "12.34 MB/s"
-- [MeterNetIn]
-- Meter=String
-- MeasureName=FormatNetIn
-- Text="%1B/s"
--
----------------------------------------------------------------------------------------------------
--
function Initialize()
  --
  -- this function is called when the measure is initialized or reloaded
  --

  -- initialize array of suffixes for scaled values
  asSuffix = { " ", " k", " M", " G", " T", " P", " E", " Z", " Y" }

  -- initialize default divisor
  nDivisor = 1024.0

  --
  -- get measure options
  --
  sMeasureName = SELF:GetOption("MeasureName")
  nPrecision = math.floor(tonumber(SELF:GetOption("Scale")))
  sFactor = string.lower(SELF:GetOption("Factor"))

  -- get a reference to the specified input measure
  msMeasure = SKIN:GetMeasure(sMeasureName)

  --
  -- do validation
  --
  -- validate Precision
  if nPrecision > 0 then
    -- OK
  else
    -- invalid or missing input
    nPrecision = 3
  end

  -- validate Factor and initialize divisor if needed
  if sFactor == "1k" then
    -- OK
  elseif sFactor == "2" or sFactor == "2k" then
    nDivisor = 1000.0
  else
    sFactor = "1"
  end

  return
end                                                                                    -- Initialize

function Update()
  --
  -- this function is called whenever the measure is updated
  --
  -- Examples:  nValue = 3.141592654, nPrecision = 7, sFactor = "1":  output = "3.141593 "
  --            nValue = 31.41592654, nPrecision = 7, sFactor = "1":  output = "31.41593 "
  --            nValue = 314.1592654, nPrecision = 7, sFactor = "1":  output = "314.1593 "
  --            nValue = 3141.592654, nPrecision = 7, sFactor = "1":  output = "3.141593 k"
  --            nValue = 31415926.54, nPrecision = 7, sFactor = "1":  output = "31.41593 M"
  --            nValue = 31415926.54, nPrecision = 4, sFactor = "1":  output = "31.42 M"
  --            nValue = 31415926.54, nPrecision = 3, sFactor = "1":  output = "31.4 M"
  --            nValue = 31415926.54, nPrecision = 2, sFactor = "1":  output = "31 M"
  --            nValue = 31415926.54, nPrecision = 1, sFactor = "1":  output = "31 M" (precision too small)
  --

  -- initialize local vars
  local nDigitsAfterDecimal = 0
  local nDigitsBeforeDecimal = 0
  local nDivCount = 1
  local sPattern = ""
  local sText = ""

  -- get the current value of the input measure
  local nValue = msMeasure:GetValue()

  -- if minimum value is K, divide value by divisor
  if sFactor == "1k" or sFactor == "2k" then
    nValue = nValue / nDivisor
    nDivCount = nDivCount + 1
  end

  --
  -- format the value as text
  --
  while true do
    if math.abs(nValue) < nDivisor then
      nDigitsBeforeDecimal = math.max(1, math.floor(math.log10(math.abs(nValue))) + 1)
      nDigitsAfterDecimal = math.max(0, nPrecision - nDigitsBeforeDecimal)

      -- get formatting directive
      sPattern = "%." .. nDigitsAfterDecimal .. "f"
      sText = string.format(sPattern, nValue)
      break

    else
      nValue = nValue / nDivisor
      nDivCount = nDivCount + 1
    end
  end

  return(sText .. asSuffix[nDivCount])
end                                                                                        -- Update
This script can used to call the function on demand via a CommandMeasure bang.

Code: Select all

--
-- FixedPrecisionFormat v1.0 by SilverAzide
--
-- This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License.
--
----------------------------------------------------------------------------------------------------
--
-- Rainmeter's String meter formats numbers using a "variable precision, fixed scale" methodology.
-- ("Precision" is the total number of digits, not including signs and decimals, and "scale" is the
-- number of digits after the decimal.)
--
--   Examples:  3.141      (precision=4, scale=3; i.e., "NumOfDecimals=3")
--              314.159    (precision=6, scale=3)
--              3141.593   (precision=7, scale=3)
--              314159.265 (precision=9, scale=3)
--
-- This style of formatting means that the total number of digits in the resulting string, and thus
-- the string's length, is somewhat unpredictable.  Designers must include extra whitespace after
-- the numbers to account for large values, so if additional design elements follow the meter, there
-- is a possibility that the data can overflow one meter into another.  Also, when using this in
-- meters with rapidly changing values, the meter tends to show numbers that bounce around, as large
-- and small values alternate.
--
-- This script's FormatNumber function uses a "fixed precision, variable scale" methodology to
-- format numbers.  This results in values that have a more predicable string length and can lead to
-- a cleaner looking display.
--
--   Examples:  3.14159   (precision=6, scale=5)
--              314.159   (precision=6, scale=3)
--              3141.53   (precision=6, scale=2)
--              314159    (precision=6, scale=0)
--
-- This function implies use of "AutoScale" (i.e., all values will be scaled).  If scaling is not
-- desired, use the existing Rainmeter String meter instead.  Note that in theory negative scales
-- are valid; e.g., 31415900  (precision=6, scale=-2).  However, because of autoscaling, this is of
-- little value and is not supported.
--
-- REMEMBER:
--
--   This measure returns a formatted STRING.  Numeric formatting options in String meters will
--   not have any effect (NumOfDecimals, Scale, AutoScale, etc.) and should not be used.
--
--
-- Usage
--
--   FormatNumber(InputValue, Precision, Factor, VariableName)
--
--     InputValue
--
--       The value of a measure, variable, or formula to be formatted.
--
--     Precision
--
--       Specifies the numeric precision (i.e., the total number of digits in the number).  The
--       default is 3.
--
--     Factor
--
--       Specifies the scale factor.  Same as the Rainmeter String meter's "AutoScale" option:
--
--         1:  Scales by 1024 (default).
--         1k: Scales by 1024 with kilo as the lowest unit.
--         2:  Scales by 1000.
--         2k: Scales by 1000 with kilo as the lowest unit.
--
--       NOTE:
--
--         The value returned by the plugin includes a space between the scaled number and the scale
--         unit abbreviation.  To remove this space simply add Substitute=" ":"" to the measure.
--
--     VariableName
--
--       The name of a varible used to hold the formatted string.
--
--
-- Example skin:
--
-- [Variables]
-- ; create variable to hold formatted text
-- TextNetIn=""
--
-- [FormatScript]
-- Measure=Script
-- ScriptFile=FixedPrecisionFormat.lua
--
-- ; measure network inbound bytes/sec, format value when changed; e.g., "12.34 M"
-- [MeasureNetIn]
-- Measure=NetIn
-- OnChangeAction=[!CommandMeasure FormatScript "FormatNumber([MeasureNetIn], 4, '1k', 'TextNetIn')"]
--
-- ; display inbound bytes/sec; e.g., "12.34 MB/s"
-- [MeterNetIn]
-- Meter=String
-- Text="#TextNetIn#B/s"
-- DynamicVariables=1
--
----------------------------------------------------------------------------------------------------
--
function Initialize()
  --
  -- this function is called when the script measure is initialized or reloaded
  --

  -- initialize array of suffixes for scaled values
  asSuffix = { " ", " k", " M", " G", " T", " P", " E", " Z", " Y" }

  return
end                                                                                    -- Initialize

function FormatNumber(sInputValue, sPrecision, sFactor, sVarName)
  --
  -- This function formats a number using a "fixed precision, variable scale" methodology.  Can be
  -- called on demand via a CommandMeasure bang.
  --
  -- Where:  sInputValue = value to be formatted
  --         sPrecision  = numeric scale
  --         sFactor     = scale factor ("1", "1k", "2", "2k")
  --         sVarName    = name of variable to be updated
  --
  -- Examples:  sInputValue = 3.141592654, sPrecision = 7, sFactor = "1":  output = "3.141593 "
  --            sInputValue = 31.41592654, sPrecision = 7, sFactor = "1":  output = "31.41593 "
  --            sInputValue = 314.1592654, sPrecision = 7, sFactor = "1":  output = "314.1593 "
  --            sInputValue = 3141.592654, sPrecision = 7, sFactor = "1":  output = "3.141593 k"
  --            sInputValue = 31415926.54, sPrecision = 7, sFactor = "1":  output = "31.41593 M"
  --            sInputValue = 31415926.54, sPrecision = 4, sFactor = "1":  output = "31.42 M"
  --            sInputValue = 31415926.54, sPrecision = 3, sFactor = "1":  output = "31.4 M"
  --            sInputValue = 31415926.54, sPrecision = 2, sFactor = "1":  output = "31 M"
  --            sInputValue = 31415926.54, sPrecision = 1, sFactor = "1":  output = "31 M" (precision too small)

  -- initialize local vars
  local nDigitsAfterDecimal = 0
  local nDigitsBeforeDecimal = 0
  local nDivCount = 1
  local nDivisor = 1024.0
  local sPattern = ""
  local sText = ""

  --
  -- validation input parameters
  --
  local nValue = tonumber(sInputValue)

  -- validate Scale
  local nPrecision = math.floor(tonumber(sPrecision)) or 3
  if nPrecision > 0 then
    -- OK
  else
    -- invalid input
    nPrecision = 3
  end

  -- validate Factor and set divisor if needed
  if sFactor == "1k" then
    -- OK
  elseif sFactor == "2" or sFactor == "2k" then
    nDivisor = 1000.0
  else
    sFactor = "1"
  end

  --
  -- format the value as text
  --

  -- if minimum value is K, divide value by divisor
  if sFactor == "1k" or sFactor == "2k" then
    nValue = nValue / nDivisor
    nDivCount = nDivCount + 1
  end

  while true do
    if math.abs(nValue) < nDivisor then
      nDigitsBeforeDecimal = math.max(1, math.floor(math.log10(math.abs(nValue))) + 1)
      nDigitsAfterDecimal = math.max(0, nPrecision - nDigitsBeforeDecimal)

      -- get formatting directive
      sPattern = "%." .. nDigitsAfterDecimal .. "f"

      -- format the number
      sText = string.format(sPattern, nValue)
      break

    else
      nValue = nValue / nDivisor
      nDivCount = nDivCount + 1
    end
  end

  SKIN:Bang("!SetVariable", sVarName, sText .. asSuffix[nDivCount])

  return
end                                                                                  -- FormatNumber
Gadgets Wiki GitHub More Gadgets...
User avatar
jsmorley
Developer
Posts: 22629
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Re: Suggestion: String Meter enhancement - Redux

Post by jsmorley »

Interesting conundrum.

Many, in fact most, measures won't fire an OnChangeAction when the skin is first refreshed, as the first value they return is not a "change" as such. Philosophical arguments aside, Rainmeter doesn't treat "nothing to not-nothing" as a change in this context.

What you might do is:

[MeasureSomeThing]
Measure=SomeThing
IfCondition=1
IfTrueAction=[!CommandMeasure ScriptMeasure "Function()"]
OnChangeAction=[!CommandMeasure ScriptMeasure "Function()"]

That IfCondition will cause the IfTrueAction to fire the script if in effect "1" is equal to "1". In my experience, it is... However, since IfTrueAction is only fired once, then again only if and when the condition changes from "false to true", it will only ever be fired one time on the load or refresh of the skin. From then on, the script is controlled by the OnChangeAction.

You can see it in action with this:

[MeasureCPU]
Measure=CPU
IfCondition=1
IfTrueAction=[!Log "Measure Initialized"]
OnChangeAction=[!Log "Measure Changed"]

ANY "always true" condition would work:
IfCondition=MeasureName = MeasureName
IfCondition=0 < 1

While perhaps not as intuitive, that is in effect a roll-your-own OnInitAction.

I'm not sure an actual OnInitAction will make sense, as there ARE some measures, those that are threaded plugins like WebParser, that will fire OnChangeAction initially, as they are designed to have an initial value of "", and the first time they receive actual data, that will in fact be seen as a "change".

So it might be hard to get consistent, easy-to-understand behavior with an OnInitAction on measures. Having a measure that fires the action twice, both OnInitAction and OnChangeAction, when you refresh the skin is not what you want, and it might be hard for users to wrap their heads around when to use it and when not to. It would be a moving target, as any future 3rd-party plugin might or might not behave either way.
User avatar
SilverAzide
Rainmeter Sage
Posts: 2604
Joined: March 23rd, 2015, 5:26 pm

Re: Suggestion: String Meter enhancement - Redux

Post by SilverAzide »

Aha... the "IfCondition=1" idea ought to do the trick nicely. I'll give that a try! Thanks!
Gadgets Wiki GitHub More Gadgets...
User avatar
jsmorley
Developer
Posts: 22629
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Re: Suggestion: String Meter enhancement - Redux

Post by jsmorley »

SilverAzide wrote:Aha... the "IfCondition=1" idea ought to do the trick nicely. I'll give that a try! Thanks!
Glad to help. I personally like IfCondition=42. I like to get as many obscure "Hitchhiker's Guide" references into my skins as I can.
TGonZo
Posts: 68
Joined: April 25th, 2015, 8:19 pm
Location: Virginia

Re: Suggestion: String Meter enhancement - Redux

Post by TGonZo »

Hi all,

SilverAzide that is some masterful coding there. I was working on this just using measures in Rainmeter. I had it working, but it took at least 8 measures. Not bad, it worked, but I didn't like it. Then I saw this posting. Very nice.

I had not started to look into Lua script until now. So, I looked over your script to help me understand it all. I have a couple of changes I hope you like. I know there was someone out there that pointed out not liking the while true loop. If I may, here is what I changed. I'm running with this change, and it's working for me. I'm using your CommandMeasure bang version from above.


Original code

Code: Select all

  while true do
    if math.abs(nValue) < nDivisor then
      nDigitsBeforeDecimal = math.max(1, math.floor(math.log10(math.abs(nValue))) + 1)
      nDigitsAfterDecimal = math.max(0, nPrecision - nDigitsBeforeDecimal)

      -- get formatting directive
      sPattern = "%." .. nDigitsAfterDecimal .. "f"

      -- format the number
      sText = string.format(sPattern, nValue)
      break

    else
      nValue = nValue / nDivisor
      nDivCount = nDivCount + 1
    end
  end

My suggested changes.

Code: Select all

  while (math.abs(nValue) > nDivisor) and (nDivCount < 9) do
    nValue = nValue / nDivisor
    nDivCount = nDivCount + 1
  end

  nDigitsBeforeDecimal = math.max(1, math.floor(math.log10(math.abs(nValue))) + 1)
  nDigitsAfterDecimal = math.max(0, nPrecision - nDigitsBeforeDecimal)

  -- get formatting directive
  sPattern = "%." .. nDigitsAfterDecimal .. "f"

  -- format the number
  sText = string.format(sPattern, nValue)
It's only a minor change, but it seems to clean the loop up a bit. I also added the nDivCount check so it does not over run the asSuffix array. Not that any of us will hit Yottabytes right now, but maybe someday.

I hope you approve.
And thanks again for the awesome script.
User avatar
SilverAzide
Rainmeter Sage
Posts: 2604
Joined: March 23rd, 2015, 5:26 pm

Re: Suggestion: String Meter enhancement - Redux

Post by SilverAzide »

TGonZo,
Thanks for the assist! Glad you were able to make use of this. Funny thing, within minutes of your posting I had just made an enhancement to the function to support the ability to disable scaling (to fully sync with Rainmeter's "AutoScale" setting). You can now pass a "0" as the scale factor value to disable scaling, which should come in handy if you are not measuring bytes, but instead stuff like stock prices or percentages and don't want to show "kilo-dollars" or "mega-percents".
:)

I merged your suggestions into the changes I made. I've included the full script here in case anyone else is interested.

Code: Select all

--
-- FixedPrecisionFormat v1.1.0 by SilverAzide
--
-- This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License.
--
-- History:
-- 1.1.0 - 2015-05-22:  Added support for scale factor "0" to match Rainmeter's AutoScale option.
--                      Thanks to TGonZo for code improvements.
-- 1.0.0 - 2015-05-09:  Initial release.
--
----------------------------------------------------------------------------------------------------
--
-- Rainmeter's String meter formats numbers using a "variable precision, fixed scale" methodology.
-- ("Precision" is the total number of digits, not including signs and decimals, and "scale" is the
-- number of digits after the decimal.)
--
--   Examples:  3.141      (precision=4, scale=3; i.e., "NumOfDecimals=3")
--              314.159    (precision=6, scale=3)
--              3141.593   (precision=7, scale=3)
--              314159.265 (precision=9, scale=3)
--
-- This style of formatting means that the total number of digits in the resulting string, and thus
-- the string's length, is somewhat unpredictable.  Designers must include extra whitespace after
-- the numbers to account for large values, so if additional design elements follow the meter, there
-- is a possibility that the data can overflow one meter into another.  Also, when using this in
-- meters with rapidly changing values, the meter tends to show numbers that bounce around, as large
-- and small values alternate.
--
-- This script uses a "fixed precision, variable scale" methodology to format numbers.  This results
-- in values that have a more predicable string length and can lead to a cleaner looking display.
--
--   Examples:  3.14159   (precision=6, scale=5)
--              314.159   (precision=6, scale=3)
--              3141.53   (precision=6, scale=2)
--              314159    (precision=6, scale=0)
--
-- Note that in theory negative scales are valid; e.g., 31415900  (precision=6, scale=-2).  However,
-- this appears to be of little value and is not supported at this time.
--
-- REMEMBER:
--
--   This script returns a formatted STRING.  Numeric formatting options in String meters may not
--   have any effect (NumOfDecimals, Scale, AutoScale, etc.) and should not be used.
--
--
-- Usage
--
--   FormatNumber(InputValue, Precision, Factor, VariableName)
--
--     InputValue
--
--       The value of a measure, variable, or formula to be formatted.
--
--     Precision
--
--       Specifies the numeric precision (i.e., the total number of digits in the number).  The
--       default is 3.
--
--     Factor
--
--       Specifies the scale factor.  Same as the Rainmeter String meter's "AutoScale" option:
--
--         0:  Disabled (default).
--         1:  Scales by 1024.
--         1k: Scales by 1024 with kilo as the lowest unit.
--         2:  Scales by 1000.
--         2k: Scales by 1000 with kilo as the lowest unit.
--
--       NOTE:
--
--         The value returned by the plugin includes a space between the scaled number and the scale
--         unit abbreviation.  When scaling is disabled, the number will include a trailing space.
--         To remove this space simply add Substitute=" ":"" to the measure.
--
--     VariableName
--
--       The name of a varible used to hold the formatted string.
--
--
-- Example skin:
--
-- [Variables]
-- ; create variable to hold formatted text
-- TextNetIn=""
--
-- [FormatScript]
-- Measure=Script
-- ScriptFile=#@#FixedPrecisionFormat.lua
--
-- ; measure network inbound bytes/sec, format value when changed; e.g., "12.34 M"
-- [MeasureNetIn]
-- Measure=NetIn
-- OnChangeAction=[!CommandMeasure FormatScript "FormatNumber([MeasureNetIn], 4, '1k', 'TextNetIn')"]
--
-- ; display inbound bytes/sec; e.g., "12.34 MB/s"
-- [MeterNetIn]
-- Meter=String
-- Text="#TextNetIn#B/s"
-- DynamicVariables=1
--
----------------------------------------------------------------------------------------------------
--
function Initialize()
  --
  -- this function is called when the script measure is initialized or reloaded
  --

  -- initialize array of suffixes for scaled values
  asSuffix = { " ", " k", " M", " G", " T", " P", " E", " Z", " Y" }

  return
end                                                                                    -- Initialize

function FormatNumber(sInputValue, sPrecision, sFactor, sVarName)
  --
  -- This function formats a number using a "fixed precision, variable scale" methodology.  Can be
  -- called on demand via a CommandMeasure bang.
  --
  -- Where:  sInputValue = value to be formatted
  --         sPrecision  = numeric scale
  --         sFactor     = scale factor ("0", "1", "1k", "2", "2k")
  --         sVarName    = name of variable to be updated
  --
  -- Examples:  sInputValue = 3.141592654, sPrecision = 7, sFactor = "1":  output = "3.141593 "
  --            sInputValue = 31.41592654, sPrecision = 7, sFactor = "1":  output = "31.41593 "
  --            sInputValue = 314.1592654, sPrecision = 7, sFactor = "1":  output = "314.1593 "
  --            sInputValue = 3141.592654, sPrecision = 7, sFactor = "1":  output = "3.141593 k"
  --            sInputValue = 31415926.54, sPrecision = 7, sFactor = "1":  output = "31.41593 M"
  --            sInputValue = 31415926.54, sPrecision = 4, sFactor = "1":  output = "31.42 M"
  --            sInputValue = 31415926.54, sPrecision = 3, sFactor = "1":  output = "31.4 M"
  --            sInputValue = 31415926.54, sPrecision = 2, sFactor = "1":  output = "31 M"
  --            sInputValue = 31415926.54, sPrecision = 1, sFactor = "1":  output = "31 M" (precision too small)
  --            sInputValue = 3141.592654, sPrecision = 7, sFactor = "0":  output = "3141.593 "
  --

  -- initialize local vars
  local nDigitsAfterDecimal = 0
  local nDigitsBeforeDecimal = 0
  local nDivCount = 1
  local nDivisor = 1024.0
  local sPattern = ""
  local sText = ""

  --
  -- validate input parameters
  --
  local nValue = tonumber(sInputValue)

  -- validate Scale
  local nPrecision = math.floor(tonumber(sPrecision)) or 3
  if nPrecision > 0 then
    -- OK
  else
    -- invalid input
    nPrecision = 3
  end

  -- validate Factor and set divisor if needed
  if sFactor == "1" or sFactor == "1k" then
    -- OK
  elseif sFactor == "2" or sFactor == "2k" then
    nDivisor = 1000.0
  else
    sFactor = "0"
    nDivisor = 1.0
  end

  --
  -- format the value as text
  --

  -- if minimum value is K, divide value by divisor
  if sFactor == "1k" or sFactor == "2k" then
    nValue = nValue / nDivisor
    nDivCount = nDivCount + 1
  end

  while (math.abs(nValue) > nDivisor and nDivCount < 9 and nDivisor > 1.0) do
    nValue = nValue / nDivisor
    nDivCount = nDivCount + 1
  end

  nDigitsBeforeDecimal = math.max(1, math.floor(math.log10(math.abs(nValue))) + 1)
  nDigitsAfterDecimal = math.max(0, nPrecision - nDigitsBeforeDecimal)

  -- get formatting directive
  sPattern = "%." .. nDigitsAfterDecimal .. "f"

  -- format the number
  sText = string.format(sPattern, nValue)

  --
  -- save the result to the variable and exit
  --
  SKIN:Bang("!SetVariable", sVarName, sText .. asSuffix[nDivCount])

  return
end                                                                                  -- FormatNumber
I hope you find this helpful!
Gadgets Wiki GitHub More Gadgets...
User avatar
Yincognito
Rainmeter Sage
Posts: 7157
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Suggestion: String Meter enhancement - Redux

Post by Yincognito »

moshi wrote:just for fun, please ignore:
.................
(formula stolen from smurfier)
Just for fun, please ignore this as well :D :

- I think in your "stolen" formula, something is wrong, moshi ... Log(x)<=0 not only when x=0, but also when x is between 0 and 1. Therefore, for sub-unit values, the method yields wrong results: due to the negative powers of 1024, things are actually getting multiplied, instead of being divided. The condition should be changed from =0 to <=1.
- I don't know why smurfier thought of writing 10*log(2) there, as it doesn't ring a bell anywhere in our heads, lol. The value is simply log(1024) and this fits the general idea of the formula pretty well. It also becomes easier to change things when applying an "autoscale" of 2 (by 1000) instead of an "autoscale" of 1 (by 1024): you simply have to replace 1024 with 1000 in both places (including the former 10*log(2) which was previously "transformed" into log(1024)).
- AutoScale, Prefix and Postfix aren't really necessary in the meter, as things can be done in just one step, in the [MeasureMem2] (aka my [MeasureMemToolTip]) measure. I adapted the method a little bit, and it deviates from the original intended purpose of SilverAzide, but gets its job very well for my need : autoscale + right align in tooltips (where formatting isn't possible, only by the somewhat "restricted" section variables). And to potentially answer to jsmorley, the tooltip font doesn't change much in tooltips, so no need to bother with "different" monospaced or proportional fonts - you just have to find the right settings for the default one (which is proportional, but with some added spaces to fix the difference between wider and narrower characters you get the job done).

Here's what I've come up with:

Code: Select all

[Variables]
Decimals=2

[MeasureMem]
Measure=PhysicalMemory

[MeasureMemToolTip]
Measure=Calc
Formula=Round(MeasureMem/1024**((MeasureMem<=1?1:Ceil(Log(MeasureMem)/Log(1024)))-1),#Decimals#)+(MeasureMem<=1?1:Ceil(Log(MeasureMem)/Log(1024)))*(1/(10**(#Decimals#+1)))
RegExpSubstitute=1
Substitute="^([\s\S]*)$":"   \1","^.*(.{8})$":"\1"," ":"  ","1$":"     ","2$":"   k","3$":" M","4$":"  G","5$":"  T","6$":"  P","7$":"  E","8$":"  Z","9$":"  Y"

[MeterTime]
Meter=String
MeasureName=MeasureMem
ToolTipText=[MeasureMemToolTip]
Text="%1"
-Decimals2 variable is the 'precision'.
- To make this a "one step job" and avoid using the AutoScale option (which can't be manipulated in tooltips anyway), I had to somehow "memorize" the metric prefix inside the formula. I've done this by adding the +(MeasureMem<=1?1:Ceil(Log(MeasureMem)/Log(1024)))*(1/(10**(#Decimals#+1))) part to it. What it does is adding an additional decimal at the end of the number (after those from 'precision') which holds the "index" of the metric prefix (1 for none, 2 for kilo-, etc.). Given the fact that the index starts at 1, this trick is also "forcing" the resulting string to have decimals, even if 'precision' is 0, thus avoiding the need to add multiple . and to merge them afterwards in the string, like in moshi's code.
- Then, in the Substitute, besides adding some spaces for the "right align" (sometimes more, to compensate for the difference between the width of k and M, for example), I convert the above index to the corresponding metric prefix string. The excess spaces also helps in "right aligning" the metric prefixes as well.

There you go. No Lua/bua or anything for a simple thing that can be done in a simple measure. Before this, I tried doing all of it in the original measure (aka [MeasureMem]), but due to some glitches with DynamicVariables=1 which changed my "computed" measure on the fly and some section variable divisor issues discussed here, I had to take this route. The only inconvenient is that I have to have additional measures (aka [MeasureMemToolTip]) besides the original ones (aka [MeasureMem]). But then, Calc measures are not that resource hungry, after all...

P.S. I think the above could work for negative values too, if one replaces the MeasureMem occurences in the formula with Abs(MeasureMem) and storing the sign via the metric index storing trick. And yes, I know the above is not exactly what SilveAzide worked on, but thought these issues are related, so I posted here.
Profiles: Rainmeter ProfileDeviantArt ProfileSuites: MYiniMeterSkins: Earth
User avatar
Yincognito
Rainmeter Sage
Posts: 7157
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Suggestion: String Meter enhancement - Redux

Post by Yincognito »

Not really. The original formula from smurfier and moshi deserve the credit. I just adapted the whole thing to my needs.
By the way, a DynamicVariables=1 is required on the meter... :oops:
Profiles: Rainmeter ProfileDeviantArt ProfileSuites: MYiniMeterSkins: Earth