Dynamic Javascript for Multi-Variable Calculations, Used in Loan Calculation Tables

LoanGrid.netThere’s an interesting site I developed a while back for when I was refinancing a mortgage loan.  I’d conceptualized it years ago, but hadn’t actually set aside the time to build it out.  That changed recently.

I noticed that most loan/mortgage calculators were offering one calculation at a time and then showing extraneous details about that single calculation.  However, when I’m talking to my lender, I want to be able to jump from one scenario to the next with ease, and I don’t always care about the whiz-bang charts and graphs, etc.  Furthermore, considering the simplicity of these calculations, I didn’t see much need for a round-trip to the server each time a calculation is requested; most loan calculators online force a submit to the server for that single calculation they return.

Embarking on some deep(ish) javascript immersion, I developed LoanGrid.net.  It’s heavy on jQuery, including tabs, sliders, hide/show effects, etc.  But it’s also pretty light on page weight (about 50 Kb if you don’t count the jQuery CDN files that are utilized).  And there are no additional requests to the server once you get the initial page.

For those curious, the basic amortization formula is here on Wikipedia.  I’m just applying that a few dozen times to make the scenario grid, and then calculating a full amortization table for the specific scenario once the inputs stop changing.

I can talk more about some finer points of the technology at some point (e.g. the Revealing Module Pattern for encapsulation), but this is just a generic announcement.  Since this appears to be a somewhat novel approach, I’m more interested in where else such a tool would make sense.

If you have any bright ideas, please share them in the comments.  Thanks!

Fix Firefox Empty Printer List on Debian Squeeze 64-bit

Firefox icon

I recently installed Debian Squeeze (6.0.3) 64-bit, running XFCE.  While using Firefox 9 (and 10), I found that my CUPS printers weren’t showing up in Firefox’s print dialog box.  All I wanted to do was print to PDF! And a nearly identical configuration on my 32-bit Debian Squeeze setup was working just fine.

Running Firefox from the bash command line pointed me in the right direction by generating “ELFCLASS64” errors like the following:

Gtk-Message: Failed to load module “canberra-gtk-module”: /usr/lib/gtk-2.0/modules/libcanberra-gtk-module.so: wrong ELF class: ELFCLASS64
(firefox:13517): Gtk-WARNING **: /usr/lib/gtk-2.0/2.10.0/engines/libxfce.so: wrong ELF class: ELFCLASS64
(firefox:13517): Gtk-WARNING **: /usr/lib/gtk-2.0/2.10.0/engines/libxfce.so: wrong ELF class: ELFCLASS64
/usr/lib/gio/modules/libgvfsdbus.so: wrong ELF class: ELFCLASS64
Failed to load module: /usr/lib/gio/modules/libgvfsdbus.so
LoadPlugin: failed to initialize shared library /usr/lib/gnash/libgnashplugin.so [/usr/lib/gnash/libgnashplugin.so: wrong ELF class: ELFCLASS64]

Thanks to some reading, and thanks to Oleg Cherkasov’s comment #22 here, I was able to get it working.  Specifically, the following export just prior to launching Firefox seems to do the trick:

export GTK_PATH=/usr/lib32/gtk-2.0/

I presume this overrides the default GTK settings by pointing instead to the lib32 GTK. Works for me!

(Mind you, IceWeasel 3.5.x was able to find the printers on its own without this fix.)

I chose not to add this export to my user profile because I wasn’t sure what the broader impacts of the GTK override might be. So nowadays I just launch Firefox from a shell script that does this export immediately prior to kicking off the Firefox executable.

Excel VBA Range to CSV (Range2Csv) Function

Do you ever have to take a list of values provided in Excel (or raw text) and massage them into a comma-separated list?  I find myself doing this A LOT.  And it usually involves taking a very round-about approach of pasting into a text program and getting creative with find/replace.

Side note: use “^p” in MS Word’s find/replace dialog as code for a carriage return.  “^t” is code for a tab character.  Not ideal, but sometimes MS Word is the closest tool at hand.

Anyway, I’d done that enough, and I decided to write a custom Excel VBA function called “Range2Csv” to comma-delimit a range of cells.  (You can actually use any delimiter you choose; pipes, semi-colons, etc., doesn’t need to be a comma).  It’s not a bullet-proof function, because bullet-proof was not my goal (i.e., it doesn’t conditionally wrap cell values in quotations, and it always removes the delimiter character if it appears in the values, etc).  I already had clean data and just needed to make something work, and this function did the job for my basic use case.

Feel free to make it work for you too.  Suggestions are welcome in the comments below… although I’m not sure put a lot more time into this.  The code is heavily commented so you should be able to customize it yourself.

Range2Csv In Action

Range2Csv In Action

Option Explicit
'* PURPOSE: Concatenates range contents into a
'*          delimited text string
'* FUNCTION SIGNATURE: Range2Csv(Range, String)
'*    Range  - the range of cells whose contents
'*             will be included in the CSV result
'*    String - delimiter used to separate values
'*             (Optional, defaults to a comma)
'* AUTHOR: www.dullsharpness.com
'* NOTES: This function strips occurrences of the
'*        delimiter from within the cell values, so
'*        choose a delimiter that doesn't appear
'*        in the cell values or that you don't mind
'*        removal of.  For example, running Range2Csv
'*        with a "," delimiter on a cell that
'*        contains "New York, NY" will cause the
'*        CSV result string to contain "New York NY"
'*        with the comma removed.
'*        Range2Csv ignores blank cells in the Range.
'*        This function was designed for a very
'*        simple use case, and was only slightly
'*        modified for publication afterward in a
'*        way that makes it more versatile. That is,
'*        it doesn't attempt to be fail-proof or
'*        robust.  Modify it as you see fit if you
'*        need something stronger.
Public Function Range2Csv(inputRange As Range, Optional delimiter As String)
  Dim concattedList As String 'holder for the concatted CSVs
  Dim rangeCell As Range      'holder cell used in For-Each loop
  Dim rangeText As String     'holder for rangeCell's text

  'default to a comma delimiter if none is provided
  If delimiter = "" Then delimiter = ","

  concattedList = ""          'start with an empty string

  'Loop through each cell in the range to append valid contents
  For Each rangeCell In inputRange.Cells

    rangeText = rangeCell.Value 'capture the working value

    'Only operate on non-blank cells (i.e. Length > 0)
    If Len(rangeText) > 0 Then
      'Strip any delimiters contained w/in the value itself
      rangeText = WorksheetFunction.Substitute(rangeText, delimiter, "")

      If (Len(concattedList) > 0) Then
        'prepend a delimiter to the new value if we
        'already have some list items
        concattedList = concattedList + delimiter + rangeText
        'else if the list is blank so far,
        'just set the first value
        concattedList = rangeText
      End If
    End If

  Next rangeCell

  'Set the return value
  Range2Csv = concattedList

End Function

jQuery Hosted UI Themes and Code

This post lists simple copy/paste boilerplate jQuery HTML from which to pull <HEAD> entries.  Utilize your choice of CDN from Google, Microsoft, or jQuery.

And if you want some pre-made boilerplate pages to use as starters, try the ones listed in my jQuery CDN Boilerplate Starter Templates post.

Hosted on the Google  CDN:

<!-- CDN COPIES OF STANDARD JQUERY THEMES (all of these are available in v1.8.16) -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/base/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/black-tie/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/blitzer/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/cupertino/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/dark-hive/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/dot-luv/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/eggplant/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/excite-bike/jquery-ui.css" type="text/css" rel="stylesheet" />
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/flick/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/hot-sneaks/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/humanity/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/le-frog/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/mint-choc/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/overcast/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/pepper-grinder/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/redmond/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/smoothness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/south-street/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/start/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/sunny/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/swanky-purse/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/trontastic/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/ui-darkness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/ui-lightness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/vader/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- CDN COPY OF JQUERY BASE v1.6.4 (unminified version is commented out) -->
<!-- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.js" type="text/javascript"></script> -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>

<!-- CDN COPY OF JQUERY UI 1.8.16 (unminified version is commented out) -->
<!-- <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.js" type="text/javascript"></script> -->

<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>

 <!-- Sample page initiation code; executes after the DOM loads -->
 $(document).ready(function() {
 //Initialization script goes here

Hosted on the Microsoft CDN:

<!-- CDN COPIES OF STANDARD JQUERY THEMES (all of these are available in v1.8.16) -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/base/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/black-tie/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/blitzer/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/cupertino/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/dark-hive/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/dot-luv/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/eggplant/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/excite-bike/jquery-ui.css" type="text/css" rel="stylesheet" />
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/flick/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/hot-sneaks/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/humanity/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/le-frog/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/mint-choc/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/overcast/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/pepper-grinder/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/redmond/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/smoothness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/south-street/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/start/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/sunny/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/swanky-purse/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/trontastic/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/ui-darkness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/ui-lightness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/themes/vader/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- CDN COPY OF JQUERY BASE v1.6.4 (unminified version is commented out) -->
<!-- <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.4.js" type="text/javascript"></script> -->
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.4.min.js" type="text/javascript"></script>

<!-- CDN COPY OF JQUERY UI 1.8.16 (unminified version is commented out) -->
<!-- <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/jquery-ui.js" type="text/javascript"></script> -->

<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>

<!-- Sample page initiation code; executes after the DOM loads -->
$(document).ready(function() {
 //Initialization script goes here

Hosted on the jQuery CDN:

<!-- CDN COPIES OF STANDARD JQUERY THEMES (all of these are available in v1.8.16) -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/base/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/black-tie/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/blitzer/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/cupertino/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/dark-hive/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/dot-luv/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/eggplant/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<link href="http://code.jquery.com/ui/1.8.16/themes/excite-bike/jquery-ui.css" type="text/css" rel="stylesheet" />
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/flick/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/hot-sneaks/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/humanity/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/le-frog/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/mint-choc/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/overcast/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/pepper-grinder/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/redmond/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/smoothness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/south-street/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/start/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/sunny/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/swanky-purse/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/trontastic/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/ui-darkness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/ui-lightness/jquery-ui.css" type="text/css" rel="stylesheet" /> -->
<!-- <link href="http://code.jquery.com/ui/1.8.16/themes/vader/jquery-ui.css" type="text/css" rel="stylesheet" /> -->

<!-- CDN COPY OF JQUERY BASE v1.6.4 (unminified version is commented out) -->
<!-- <script src="http://code.jquery.com/jquery-1.6.4.js" type="text/javascript"></script> -->
<script src="http://code.jquery.com/jquery-1.6.4.min.js" type="text/javascript"></script>

<!-- CDN COPY OF JQUERY UI 1.8.16 (unminified version is commented out) -->
<!-- <script src="http://code.jquery.com/ui/1.8.16/jquery-ui.js" type="text/javascript"></script> -->

<script src="http://code.jquery.com/ui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>

 <!-- Sample page initiation code; executes after the DOM loads -->
 $(document).ready(function() {
 //Initialization script goes here

jQuery CDN Boilerplate Starter Templates

*** Template files with pointers to all 25 themes are linked below! ***

I use jMicrosoft_CDN_tab1Query for a handful of small projects.  I do a lot of proofs of concept, and I like to start with a fresh HTML file for each experiment.  I found myself both recreating boilerplate HTML files with the appropriate header imports ad nauseum and littering my hard drive with countless versions of the jQuery libraries.

I decided to wise up and just build an HTML file that has the bare basic jQuery and jQuery UI imports, including themes.  And so that I don’t have to litter my own hard drive with the files, I opted for the CDN route.

There are 3 well-known CDNs that serve up the jQuery libraries, and they are documented at these links: jQuery CDN, Google CDN, Microsoft CDN.

I created a separate HTML template for use with each CDN.  3 out 4 of the templates I created are specifically for the latest stable versions of jQuery on the date of this article, which are jQuery v1.6.4 and jQuery UI v1.8.16.  The 4th utilizes a trick to serve the latest stable version without hard-coding a version number, as described below.

In addition to the javascript files, each CDN also hosts the full set of out-of-the-box jQuery UI themes (e.g. Cupertino, Excite-Bike, Humanity, etc).  The template files linked below contain pointers to ALL 25 jQuery standard themes, but all are commented out except my chosen default theme: Excite-Bike.  The point of including commented out pointers is that since the <link> tags to the other themes are already included, it’s easy to substitute another standard theme by commenting out one and uncommenting another.

Something unique about Google’s CDN is that it can serve up both a specific version of jQuery/jQuery UI, as shown here:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>

<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>

Or, using an undocumented (?) feature, dynamically serve up the latest stable versions of those files by replacing the version-specific portion of the URL (“/1.6.4/” and “/1.8.16/” above) with “/1/”, like so:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript"></script>

<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js" type="text/javascript"></script>

As far as I can tell, the Microsoft CDN provides no capability to pull the latest version without hard-coding the latest version number.  The jQuery CDN does allow you to pull the latest jQuery version, but I could not find a way to get the latest jQuery UI version without hard-coding the version into the script “src” URL.  The latest stable jQuery library on jQuery’s CDN can be linked into your HTML as shown here:

<script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>

I don’t suggest using any links to the “latest” versions in a production application since features could become unsupported or inconsistent from one version to the next. Using the latest is, however, a very convenient way to stay on the bleeding edge in your experimental work without having to change any of your boilerplate HTML.

The configured HTML files I created are linked below.  I suggest you right-click on these links and “Save As…” to your hard drive for your own use.

The comments within the source should make it clear where you can edit/remove content from the skeleton HTML.

Create a Distinct Ordered List From an Unordered Duplicate List Using DOS (MS-DOS)

As I’ve stated in previous posts, I’m often forced to use minimalist tools to accomplish a job. In this installment, I challenged myself to deduplicate a list of random numbers (or any values, really) and output just the distinct ones into a new file. It’s very easy in SQL (“Select distinct…”), but there’s a little more to it when DOS is your only option. Not many people will find themselves limited in such a way, but as with many of my posts, this is more about the mental exercise and the application of techniques than it is about the end product.

To see how this works, start with a file that has about 100 lines, each line beginning with a random number between 1 and 50 inclusive. You can generate this list using Microsoft Excel. Enter the formula “=INT(RAND()*50)+1” into cell A1 and copy it down the next 99 rows. Voila: a set of random integers that’s sure to have some duplicates. Copy/paste (or just save) this set into a text file named “UnsortedDupes.txt”.

Drop the following script into a batch file within the same directory as “UnsortedDupes.txt”

@Echo Off
REM AUTHOR: www.dullsharpness.com

REM Read up on Delayed Variable Expansion in the SET command's help

REM Initialize the variable that will hold previous value
REM (choose a value that you won't encounter in your list)
set prev=bogusvalue

REM Pipe the file into a sorter, and for each full sorted line, cast aside
REM the lines that match the previous line.
for /F %%i in ('type UnsortedDupes.txt ^| sort') Do (
   echo PREV1=%prev%, VAL=%%i, PREV2="!prev!"
   If NOT "!prev!"=="%%i" (echo %%i>> DeDuped.txt)
   set prev=%%i

REM Turn off the SETLOCAL settings

After you run the batch file, you’ll find that all of your distinct/deduplicated values have been echoed into “DeDuped.txt.”

Worth noting:

  • The reason this script works, which I presume is obvious, is that when we iterate over a list of sorted duplicate values, each time the value we’re processing differs from the previous value we processed, we recognize that it’s a new unique value and echo it to the deduped list. And each time the value we’re processing matches the previous value processed, we know we’ve already recorded it, so we ignore it.

  • Delayed variable expansion is required for this script to work. Otherwise, the PREV variable won’t get assigned its new value each time through the loop. Behavior of DOS variable expansion is discussed in more detail here.

  • I made the sort operation more complicated (and perhaps less efficient) than it needed to be because I piped standard output into it using the TYPE command. I did this because I wanted to demonstrate the technique of piping within the FOR command, and specifically the requirement to escape the pipe character with a caret “^|”. An easier way to perform the same thing is with “sort UnsortedDupes.txt”, and the SORT help listing says that’s a faster way too.

  • Using numbers for this exercise exposes an imperfection in the SORT command: it sorts alphabetically instead of numerically. The result, for example, is that “10” comes before “2”. There does not appear to be a way to overcome the SORT command’s limitation.

  • This example shows how to dedupe a list of single values, but the FOR command allows you to examine a specific field from a delimited row. And SORT allows you to specify which character in the row to start examining. Therefore, applying either one of those tactics would allow you to deduplicate your list based on a specific field or character number. Review the help contents of FOR and SORT to learn how to use those capabilities.

That’s all for now.

Elapsed Timer Using Pure DOS (MS-DOS)

A while back, I discussed a Windows Script Host script for printing elapsed time updates to the terminal screen. It was more a mental exercise for me than something I thought would wow my readers, but a few people found it useful, which I was pleased to learn about in the comments. As mentioned in that previous post, some of the machines I work on everyday are limited in their installed software and in their ability to download the web’s finest utilities, which was the impetus for my creating such a script. But suppose you’re even more of a purist and would like to run such a script using purely DOS? The script in the listing below will do it for you.

One technique worth highlighting here is my use of the poor man’s “SLEEP” command near the bottom, which is mimicked by pinging a bogus IP address. Since DOS has no “sleep” mechanism, leveraging the ping timeout is one way to simulate sleep. Also useful is the technique for extracting hours, minutes, and seconds from the current time variable (see a separate note about that below).

@Echo Off
REM AUTHOR: www.dullsharpness.com
REM ~~~~~~~~~~~~~~~~~~~~~
REM     Set Variables Here
REM ~~~~~~~~~~~~~~~~~~~~~
REM Ping interval is the timeout in seconds before a ping tries again.
REM Therefore this variable dictates how often the time gets
REM printed to the screen. Consider it the "sleep" interval.
REM We use a bogus IP because we want the ping to timeout.
REM ~~~~~~~~~~~~~~~~~~~~~
REM     END User Variables
REM ~~~~~~~~~~~~~~~~~~~~~

REM Convert the ping interval seconds to millseconds

REM Print Current Time
echo. | time | find /i "current"

REM Determine the start time values
FOR /F "usebackq tokens=1,2,3 delims=:" %%i in (`echo %Time:~0,8%`) DO (
 set START_HOUR=%%i

REM Clean up leading zeroes if there are any,
REM otherwise calculations get screwy
If %START_HOUR:~0,1%==0 set START_HOUR=%START_HOUR:~1,1%

REM Come back and iterate from this point after ping timeout
FOR /F "usebackq tokens=1,2,3 delims=:" %%i in (`echo %Time:~0,8%`) DO (
 set CUR_HOUR=%%i
 set CUR_MINUTE=%%j
 set CUR_SECOND=%%k

REM Clean up leading zeroes if there are any
If %CUR_HOUR:~0,1%==0 set CUR_HOUR=%CUR_HOUR:~1,1%
If %CUR_MINUTE:~0,1%==0 set CUR_MINUTE=%CUR_MINUTE:~1,1%
If %CUR_SECOND:~0,1%==0 set CUR_SECOND=%CUR_SECOND:~1,1%

REM Calculate using a set of "CALC" values.
REM We want to leave "CUR" values untouched

REM Start by calculating seconds, which need to rollover every minute
 set /A CALC_MINUTE-=1
) Else (

REM Minutes here. They need to rollover every hour.
 set /A CALC_HOUR-=1
) Else (

REM Prepend a leading zero when necessary

REM Print the elapsed time

REM Prepare for the next iteration

REM Ping a bogus IP to mimic "sleep" behavior
ping -n 1 -w %PINGINTERVALMS% %BOGUS_IP% > nul
GOTO Iterate

Note the following:

  • The ping timeout “sleep” approach is not perfect, and your elapsed time printouts won’t always be at exactly the interval you specify. But the elapsed time calculation itself is not a function of that interval, so printed times are still correct

  • Setting the ping interval to anything less than 3 or 4 seconds can cause the printouts to be a little erratic. This is related to the previous bullet about the ping/sleep approach being imperfect

  • While the FOR loop used above (line 23) to set hour/minute/second variables is effective, it could be simplified by setting the variables like so:

    set HOURVARIABLE=%TIME:~0,2%

    This works because %TIME% is a special DOS variable that always represents the current timestamp, and the syntax shown performs string extraction. Additional special DOS variables (%DATE%, %CD%, etc.) and string extraction examples are shown in the SET command reference (type “help set” or “set /?” at a command prompt)

    The reason I chose to demonstrate the FOR loop technique in the main script is because it can be applied more universally, such as when the text strings you’re operating on are less predictable, delimited differently, etc.

  • Line 20 shows a technique I use often, wherein “echo.” issues a hard return. So I’m actually piping a hard return into the “TIME” command (which would otherwise prompt me to enter a new time). Then I’m piping that output into a “FIND” so it only grabs the line I’m interested in. Alternative approaches are:
    • “TIME /T” which prints current time as well, but not always in the format desired.
    • Echo The current time is: %TIME%” which could have produced results identical to the series of piped commands in the main listing.

  • Last Note: I think this script misbehaves when the day rolls over while it’s running. Fixing that issue is left as an exercise for the reader.

To be clear, I don’t expect this script to be one that anyone’s clamoring for. Sure, it could be useful. But it was really about the challenge of creating something seemingly simple, yet doing it with a very limited toolset. I’m publishing it primarily because it demonstrates some techniques that others might find useful when also trying to accomplish something seemingly simple using pure DOS.

As always, please leave comments if this is helpful. Enjoy.

Stopwatch Timer Using VBScript / WSH

Where I work, we run some pretty CPU-intensive reports. Users access the reports by clicking a web link, after which the server queries the hell out of database and then returns a neatly rendered PDF. Recently, we’ve run into some issues whereby the app server reaches its 5-minute timeout before the report server can spit back its PDF. The result is that the user’s session blows up and he has to start from scratch having never seen his report.

So for the last week, we’ve been hacking at a solution for this. The way it usually works throughout the day is the DBAs go and do their DBA thing by tweaking global this-that-or-the-other-parameter, and then they yell over the cubicle wall, “Ok, try it now!” Then one of the application guys clicks the link, starts a stopwatch (if he has one), and watches the clock mindlessly as it approaches the 5-minute session explosion threshold. We haven’t fixed the issue yet.

But since I had a few blocks of 5 minutes, and since I don’t have a stopwatch, I decided to create one with the tools at hand. And my tools were few because:

  • The machine used to monitor the app for this exercise is a Windows machine with little or no dev tools beyond what’s out of the box
  • I can’t download web software to this machine

That pretty much left me with a DOS prompt. But I like a challenge.

The code you see below is the quickest way I could come up with to tackle this. It uses Windows Script Host (WSH). I plan to write a follow-up to this post showing another way, which uses pure DOS.

The gist here is that when you kick this thing off, it’ll begin to spit out elapsed time updates to the console. See here:

'* DESCRIPTION: WSH/VBS Stopwatch utility
'* AUTHOR: http://www.DullSharpness.com
Option Explicit 'Option Explicit forces variable declaration

'Declare some variables
Dim Hour
Dim Minute
Dim Second
Dim StartTime
Dim Elapsed

StartTime = Timer 'Start the Timer
Wscript.Echo Time 'Print the current time

Elapsed = Timer - StartTime 'Initialize Elapsed variable (not critical)

'Modify the next 2 lines for your needs; e.g. "Do While 1 = 1" will
'make it run indefinitely until you Ctrl+C out of it
Do While Elapsed < 120   '120 means it'll only go for 2 minutes
  WScript.Sleep(1000) 'Pause for 1000 milliseconds before printing next update
  Elapsed = Timer - StartTime 'Calculate elapsed seconds
  WScript.Echo PrintHrMinSec(Elapsed) 'Print the time

Wscript.Echo Time 'Print the current time
'Main script ends here

'* This function calculates hours, minutes
'* and seconds based on how many seconds
'* are passed in and returns a nice format
Public Function PrintHrMinSec(elap)
  Dim hr
  Dim min
  Dim sec
  Dim remainder

  elap = Int(elap) 'Just use the INTeger portion of the variable

  'Using "\" returns just the integer portion of a quotient
  hr = elap \ 3600 '1 hour = 3600 seconds
  remainder = elap - hr * 3600
  min = remainder \ 60
  remainder = remainder - min * 60
  sec = remainder

  'Prepend leading zeroes if necessary
  If Len(sec) = 1 Then sec = "0" & sec
  If Len(min) = 1 Then min = "0" & min

  'Only show the Hours field if it's non-zero
  If hr = 0 Then
     PrintHrMinSec = min & ":" & sec
     PrintHrMinSec = hr & ":" & min & ":" & sec
  End If

End Function

Assuming you save this text to a file named “VBSTimer.vbs”, run it using:

cscript VBSTimer.vbs

Things to note about this script:

  • It utilizes very little CPU, and thus runs nicely in the background.
  • It may fail after 2^15 (32,768) seconds or at some other notable power of 2 number. I’m not sure.
  • You definitely want to run this using the CScript engine and NOT WScript. WScript causes an interactive MsgBox to pop up everytime it hits a “WScript.Echo” command, whereas CScript echoes passively to the DOS console.
  • You can suppress the WSH version and copyright info by issuing the “//NOLOGO” switch on the command line when you run the script

Leave a comment if you find any of this useful, and enjoy!

Stopwatch in Action

Stopwatch in Action