77

I have a table containing decimal numbers in one column. I'm looking to align them in a manner similar to a word processor's "decimal tab" feature, so that all the points sit on a vertical line.

I have two possible solutions at the moment but I'm hoping for something better...

Solution 1: Split the numbers within the HTML, e.g.

<td><div>1234</div><div class='dp'>.5</div></td>

with

.dp { width: 3em; }

(Yes, this solution doesn't quite work as-is. The concept is, however, valid.)

Solution 2: I found mention of

<col align="char" char=".">

This is part of HTML4 according to the reference page, but it doesn't work in FF3.5, Safari 4 or IE7, which are the browsers I have to hand. It also has the problem that you can't pull out the numeric formatting to CSS (although, since it's affecting a whole column, I suppose that's not too surprising).

Thus, anyone have a better idea?

MikeB
  • 1,452
  • 14
  • 28
ijw
  • 4,376
  • 3
  • 25
  • 29
  • 8
    Theoretically speaking, you can also do it in CSS: `.dp { text-align: "." }`. It's a CSS2.0 property that was dropped in CSS2.1, but may be coming back in CSS3. It's supported as well as `align="char"` though. I.e. not at all. – mercator Sep 01 '09 at 16:14
  • 1
    For those looking at this, I'd recommend a combination of answers below: print the right number of DP, make the color transparent and hide insignificant digits, after confirming numbers are monospaced in the font you're using and deciding you'll accept the risks that the actual displayed font will not have that property (small risk since it's only a beauty thing). This can be implemented server-side, in JS, or as a combination of the two (print all digits into the page, hide the insignificant ones in JS). – ijw Nov 10 '10 at 21:37
  • `align` attribute of `COL` is depricated (http://wiki.whatwg.org/wiki/Presentational_elements_and_attributes) (http://stackoverflow.com/questions/5261514/is-html-col-align-deprecated) – Ian Boyd Aug 23 '11 at 15:50
  • 2
    [text-align: ](http://www.w3.org/TR/css3-text/#character-alignment "Character-based Alignment in a Table Column") is "at risk and may be cut from the spec during its CR period if there are no (correct) implementations" ([CSS text Level 3](http://www.w3.org/TR/css3-text/)) :( – sam Jul 19 '12 at 19:15
  • 2
    Is there a more modern solution to this problem? HTML 5 has deprecated the "align" and "char" attributes of the "col" entity, and the Javascript workaround just seems so ... unfortunate. – MikeB Nov 05 '13 at 14:17
  • The problem with JS solutions is that there are environments in which no JS is possible. For instance, an HTML email. – manu May 04 '17 at 15:19
  • 1
    @sam, text-align: still seems to be in the most current working draft (https://www.w3.org/TR/css-text-4/#character-alignment) Sadly, in ten years since your comment, no browsers have implemented it yet. It is quite a common alignment for tables as well. – matchew Apr 28 '22 at 11:09

13 Answers13

23

Another way to format a number would be like this: 35<span style="visibility: hidden">.000</span>. That is, write it out with the full decimal expansion, but write the trailing decimals in invisible ink. That way you don't have to worry about the width of the decimal point.

David Leppik
  • 3,194
  • 29
  • 18
22

See this article by Krijn Hoetmer for your options and how to achieve this. The essence of this solution is to use CSS and JS to achieve this:

(function() {
  var currencies = /(\$|€|&euro;)/;
  var leftWidth = 0, rightWidth = 0;
  for(var tableCounter = 0, tables = document.getElementsByTagName("table");
      tableCounter < tables.length; tableCounter++) {
    if(tables[tableCounter].className.indexOf("fix-align-char") != -1) {
      var fCols = [], leftPart, rightPart, parts;
      for(var i = 0, cols = tables[tableCounter].getElementsByTagName("col"); i < cols.length; i++) {
        if(cols[i].getAttribute("char")) {
          fCols[i] = cols[i].getAttribute("char");
        }
      }
      for(var i = 0, trs = tables[tableCounter].rows; i < trs.length; i++) {
        for(var j = 0, tds = trs[i].getElementsByTagName("td"); j < tds.length; j++) {
          if(fCols[j]) {
            if(tds[j].innerHTML.indexOf(fCols[j]) != -1) {
              parts = tds[j].innerHTML.split(fCols[j]);
              leftPart = parts.slice(0, parts.length -1).join(fCols[j]);
              leftPart = leftPart.replace(currencies, "<span class='currency'>$1</span>");
              rightPart = fCols[j] + parts.pop();
              tds[j].innerHTML = "<span class='left'>" + leftPart + "</span><span class='right'>" + rightPart + "</span>";
            } else {
              tds[j].innerHTML = tds[j].innerHTML.replace(currencies, "<span class='currency'>$1</span>");
              tds[j].innerHTML = "<span class='left'>" + tds[j].innerHTML + "</span>";
            }
            tds[j].className = "char-align";
            var txt = document.createTextNode(tds[j].firstChild.offsetWidth);
            if(leftWidth < tds[j].firstChild.offsetWidth) {
              leftWidth = tds[j].firstChild.offsetWidth;
            }
            if(tds[j].childNodes[1]) {
              txt = document.createTextNode(tds[j].childNodes[1].offsetWidth);
              if(rightWidth < tds[j].childNodes[1].offsetWidth) {
                rightWidth = tds[j].childNodes[1].offsetWidth;
              }
            }
          }
        }
      }
    }
  }
  // This is ugly and should be improved (amongst other parts of the code ;)
  var styleText = "\n" +
      "<style type='text/css'>\n" +
      "  .fix-align-char td.char-align { width: " + (leftWidth + rightWidth) + "px; }\n" +
      "  .fix-align-char span.left { float: left; text-align: right; width: " + leftWidth + "px; }\n" +
      "  .fix-align-char span.currency { text-align: left; float: left; }\n" +
      "  .fix-align-char span.right { float: right; text-align: left; width: " + rightWidth + "px; }\n" +
      "</style>\n";
  document.body.innerHTML += styleText;
})();
table {
  border-collapse: collapse;
  width: 600px;
}
th {
  padding: .5em;
  background: #eee;
  text-align: left;
}
td {
  padding: .5em;
}
#only-css td.char-align {
  width: 7em;
}
#only-css span.left {
  float: left;
  width: 4em;
  text-align: right;
}
#only-css span.currency {
  float: left;
  width: 2em;
  text-align: left;
}
#only-css span.right {
  float: right;
  width: 3em;
  text-align: left;
}
<table id="only-css">
  <thead>
    <tr>
      <th>Number</th>
      <th>Description</th>
      <th>Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Lorem ipsum dolor sit amet</td>
      <td class="char-align">
        <span class="left">
          <span class="currency">$</span>3
        </span>
        <span class="right">,99</span>
      </td>
    </tr>
    <tr>
      <td>2</td>
      <td>Consectetuer adipiscing elit</td>
      <td class="char-align">
        <span class="left">
          <span class="currency">$</span>13
        </span>
        <span class="right">,95</span>
      </td>
    </tr>
    <tr>
      <td>3</td>
      <td>Pellentesque fringilla nisl ac mi</td>
      <td class="char-align">
        <span class="left">
          <span class="currency">$</span>4
        </span>
        <span class="right"></span>
      </td>
    </tr>
    <tr>
      <td>4</td>
      <td>Aenean egestas gravida magna</td>
      <td class="char-align">
        <span class="left">
          <span class="currency">$</span>123
        </span>
        <span class="right">,999</span>
      </td>
    </tr>
  </tbody>
</table>
Nhan
  • 3,595
  • 6
  • 30
  • 38
Dan Diplo
  • 25,076
  • 4
  • 67
  • 89
  • 3
    Useful. His article has a demo allowing you to test the solution plus JS to make it work. In summary, the JS divides the cell contents into spans one by one, measures the DP side, then use that in the stylesheet to assign a span width. It's a workaround hack, but it does do the necessary, and suggests that there's no decent way to get a browser to do it properly. – ijw Sep 01 '09 at 16:51
  • 1
    Here's an different, more general JS approach: https://github.com/ndp/align-column – ndp Apr 10 '13 at 19:00
22

I'm surprised that in 10 years of answers to this question, nobody ever mentioned the Unicode character 'FIGURE SPACE' (U+2007, &#8199;)

It's a whitespace character that is designed (by font authors, if they follow the standard) to be the same width as digits and to keep its spacing, like its more famous cousin the No-Break Space. You can use it to pad numbers to a certain string size, either on the left or on the right hand side, taking care of aligning the column or div on the same side.

Examples, both left-aligned and left-padded with figure spaces:

<p style="font-family: sans-serif">
  10000 <br>
  &#8199;&#8199;123.4 <br>
  &#8199;&#8199;&#8199;&#8199;3.141592
</p>

<p style="font-family: serif">
  10000 <br>
  &#8199;&#8199;123.4 <br>
  &#8199;&#8199;&#8199;&#8199;3.141592
</p>
Tobia
  • 17,856
  • 6
  • 74
  • 93
  • I see no (automatic) alignment here. You just put chars in here. This is like Word-Users doing "layouting" with spaces and blank-lines. This is not a valid answer. – buhtz Jul 08 '21 at 09:34
  • 1
    This is exactly the same way alignment is done in linux terminals or other text consoles, where each character has a fixed size. In most fonts the point (.) and other such punctuation have variable size, but the digits have the same size, therefore you can use FIGURE SPACE to pad them. You may not like the technique, but it's definitely valid. – Tobia Jul 09 '21 at 10:15
  • But it is not automatic. The "linux terminals" look for themself where the `.` is and calculate how much `FIGURE SPACE` is needed. But in your example/solution the user (or content creator) himself need to calculate. – buhtz Jul 09 '21 at 11:10
  • 1
    If you use comma separators in your column, like 123,456.789, this technique won't quite work (unless Unicode has a COMMA SPACE, which I'm not seeing). – devuxer Jan 21 '23 at 03:43
  • Wow! In 25 years of working with this stuff this is the first time I've heard of "FIGURE SPACE" and it made solving my problem trivial. Many thanks. – Night Owl Jul 17 '23 at 09:27
10

Cheat; benefit of this solution: also works for proportional fonts. Have one extra column and split the integer part from the decimal separator and the decimals. Then use this css and combine two columns in the header row:

table {border-collapse:collapse;}
td {padding:0px;margin:0px;border:0px;}
td+td {text-align:right;}
td, td+td+td {text-align:left;}
<table>
    <tr><th>Name</th><th colspan=2>Height</th></tr>
    <tr><td>eiffeltower</td> <td>324</td> <td></td></tr>
    <tr><td>giraffe</td> <td>5</td> <td>,30</td></tr>
    <tr><td>deer</td> <td>1</td> <td></td></tr>
    <tr><td>mouse</td> <td>0</td> <td>,03</td></tr>
</table>

Caveat: It isn't guaranteed to work. For example, on Safari 14 in 2021:

Inigo
  • 12,186
  • 5
  • 41
  • 70
ard jonker
  • 125
  • 1
  • 2
  • 29
    Tim Berners-Lee would be spinning in his grave, if he were dead. But yep, it's an answer. – ijw May 09 '11 at 22:01
  • 3
    Why would Tim Berners-Lee care about this?? This looks like a clever, robust, and standard-compliant way of achieving what the OP was after. – AVProgrammer Jan 20 '16 at 16:00
  • 17
    @AVProgrammer because the semantic cohesion of what's a conveyed value (e.g. a number here) is sacrificed. Digits on the left vs. right side of the decimal marker go into separate . While the visually rendered output may match what's desired, accessibility, contiguous text selectability, ability and similar things were completely ignored. So one can't call it standards-compliant because standards do deal with concerns other than prescribing well-formed, visually renderable markup. – Robert Monfera Jan 26 '16 at 14:13
  • Oh, because you can't select it. Got it. – AVProgrammer Jan 26 '16 at 17:19
  • 1
    @AVProgrammer Also screen readers. Also it's just wrong. – Ian Boyd Jun 21 '20 at 15:11
8

I played around with jQuery & came up with this...

$(document).ready(function() {
  $('.aBDP').each(function() {
    var wholePart, fractionPart;
    wholePart = Math.floor($(this).text()-0);
    fractionPart = Math.floor(($(this).text() % 1)*10000 + 0.5) / 10000 + "";
    html  = '<span class="left">' + wholePart + '.' + '</span>';
    html += '<span class="right">' + fractionPart.substring(2) + '</span>';
    $(this).html(html); 
  })
})
.right {
    text-align: left;
}
.left {
    float:left;
    text-align: right;
    width:10em;
}
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script>

<table width="600" border="1">  
  <tr><th></th><th>Aligned Column</th></tr>
  <tr><th>1st Row</th><td class='aBDP'>1.1</td></tr>
  <tr><th>2nd Row</th><td class='aBDP'>10.01</td></tr>  
  <tr><th>3rd Row</th><td class='aBDP'>100.001</td></tr>  
  <tr><th>4th Row</th><td class='aBDP'>1000.0001</td></tr>
</table>

It seemed to work.

GSerg
  • 76,472
  • 17
  • 159
  • 346
  • Not bad, though you need to be very sure your column has more than 10em of free space first. – ijw Sep 22 '09 at 21:56
6

can you just print the numbers so that they always have the same number of decimal places, and right align them?

Rob Fonseca-Ensor
  • 15,510
  • 44
  • 57
  • 13
    Actually, numbers in any decent font _are_ monospaced – Eric Sep 01 '09 at 18:59
  • 5
    @Eric Not all numbers are alike: http://practicaltypography.com/grids-of-numbers.html – Roman Boiko Aug 17 '14 at 07:10
  • 1
    If you're not using monospaced numbers, then what's the point of aligning them by decimal point anyway? The whole point of aligning by decimal is so that the columns of each number place lines up. – LarryBud Nov 29 '19 at 15:24
  • Even without that then it gives you a sense of numeric significance (albeit not as good as you'd get with monospaced numbers). – ijw Jun 14 '22 at 17:23
2

Thousands of years ago (or 2-3) I wrote a jQuery shim that emulates align="char" which still seems to work. It uses CSS padding and accounts for colspans, so it's moderately clever, but it's really not very pretty code (I was just starting out in javascript back then). I'd love for someone to rewrite it (and take all the credit).

In the mean time, see if this helps you: https://gist.github.com/mattattui/f27ffd25c174e9d8a0907455395d147d

Trivia: The reason that browsers don't properly support column styles is that tables are 2D data structures and the DOM (which is what Javascript and CSS operate on, and how HTML5 is defined) is purely hierarchical and therefore can't represent both columns and rows. Instead it simply defines rows and cells, and doesn't represent columns at all.

inanimatt
  • 692
  • 5
  • 9
1

I love short answers, even though the long ones are important too, so I liked;

35<span style="color:transparent">.000</span>

and would just like to add;

<TD><div style='float:right;'><?php echo number_format($totalAmount,2); ?></div></TD>

just to throw php into the mix. Much depends on fixed width fonts, still, but the latter works for me. Since data oft is already tabular, adding another table within a cell is just too much typing and hard to maintain.

Soup Cup
  • 101
  • 1
  • 5
0

If the numbers are monospaced, javascript could be used to adjust the padding on the cell (in ems), depending on the number of digits before the decimal point. Otherwise, it could be tricky.

Eric
  • 95,302
  • 53
  • 242
  • 374
  • Good point. Not quite as nice a solution as padding, but easier. – Eric Sep 01 '09 at 18:56
  • Em's are line hieght, not charachter width. You might get lucky and they are equal or you might get lucky and work out some fraction of an EM which sometimes happens to be almost exactly the same as a character width, but generally speaking this is a bad idea. – Charlie Nov 30 '10 at 23:05
  • @Charlie: Sorry, I meant `ex`, the CSS unit for the width of an 'x' (presumably equivalent to the width of numbers, which are monospaced in many fonts anyway) – Eric Dec 01 '10 at 17:03
  • 1
    You may want to use `ch` instead of `ex` or `em`; see https://developer.mozilla.org/en-US/docs/Web/CSS/length – codermonkeyfuel Oct 06 '16 at 14:21
  • @Eric: Not sure how long it's existed, but according to [the browser compatability table](https://developer.mozilla.org/en-US/docs/Web/CSS/length#Browser_compatibility) on that page, it's supported by all recent browsers. – codermonkeyfuel Oct 10 '16 at 10:39
0

I have used JavaScript to fix this issue... This is my HTML.

<body>
<table id="nadis">

</tr>
</table>

</body>

This is my JavaScript.

var numarray = ["1.1", "12.20", "151.12", 1000.23,12451];
var highetlen = 0;
for(var i=0; i<numarray.length; i++){
    var n = numarray[i].toString();
  var res= n.split(".");
  n = res[0];

  if(highetlen < n.length){
    highetlen = n.length;
  }

}

for(var j=0; j<numarray.length; j++){
    var s = numarray[j].toString();
  var res= s.split(".");
  s = res[0];

    if(highetlen > s.length){
  var finallevel = highetlen - s.length;

  var finalhigh = "";
  for(k=0;k<finallevel;k++){
  finalhigh = finalhigh+ '&#160; ';
  }
    numarray[j] = finalhigh + numarray[j];
  }
  var nadiss = document.getElementById("nadis");
  nadiss.innerHTML += "<tr><td>" + numarray[j] + "</td></tr>";
}
0

A serious trouble in the previous approaches, is that only think in visual, but do not in other needs or uses of tables as sorting or filtering, where pure data is important.

Unfortunately CSS4 are not available yet. Then a valid solution could be pass the value and units or type unit in data attributes on td cell.

<!-- HTML-->
<table>
  <tbody>
    <tr>
      <td data-value="1876.67542" data-unit="USD"></td>
    </tr>
  </tbody>
</table>

If a cell have a data value, it must read with javascript and updated to the decimal numbers that we requires.

// Javascript
let $td_value = document.querySelectorAll( 'td[data-item]' );
Array.from( $td_value ).forEach( $r => {
  $r.textContent = parseFloat( $r.getAttribute('data-value') ).toFixed(2);
});

At the end, when we have normalized data, they will looks great with mono fonts and with their units placed using css selectors as before or after.

/* CSS */
td[data-value]{
  font-family: monospace;
  text-align: right;
}
td[data-unit]::after{
  content: attr(data-unit]);
  font-size: 85%;
  padding-left: .2em;
  opacity: .6;
}

I put an extended example in: https://jsfiddle.net/jam65st/wbo63xpu/12/

J. A. Mendez
  • 67
  • 1
  • 5
0

Ugly workaround but will save you from writing a lot of code: You can find the max number in the array (list) of prices, then you can take the number of its digits and set inline style "width": (maxNumberDigits * 10)px - this is the ugly part! And the container of this data (cell if its table) should have additionally

display:flex;
justify-content: flex-end;

Result:

Result

Vishal Kumar Sahu
  • 1,232
  • 3
  • 15
  • 27
0

The function made by Krijn Hoetmer interferes with prettyPhoto ( http://www.no-margin-for-errors.com/projects/prettyphoto-jquery-lightbox-clone/ ) so I made a jQuery version. The currency part is removed as it should be made dynamic instead of replacing strings based on predefined currencies.

Needed is the empty function from phpjs: http://phpjs.org/functions/empty:392 .

The jQuery used, is version 1.6.

/* This function will align table columns on the char if in the col from the 
 * colgroup has the property 'align="char"' and a attribute 'char'. The alignment
 * is done on the first occurence of the specified char.
 * 
 * The function is inspired from:
 * 
 * http://krijnhoetmer.nl/stuff/javascript/table-align-char/
 * http://stackoverflow.com/questions/1363239/aligning-decimal-points-in-html
 */
function alignNumbers()
{
  var table; /* This will store the table currently working on . */
  var i = 0; /* Every column can have it's own width, the counter makes the class name unique. */

  /* Get all tables for which the alignment fix must be done. 
   *
   * Note: this could even be further optimized by just looking for tables where
   * there is a a col with 'align="char"'. 
   */
  $('table.fix-align-char').each(function(index)
  {
    table = $(this);

    /* All table columns are fetched to have a correct index, without it it's
     * hard to get the correct table cells.
     */
    $(this).find('col').each(function(index)
    {
      /* Only those table cells are changed for which the alignment is set to
       * char and a char is given.
       */
      if ($(this).prop('align') == 'char' && !empty($(this).attr('char')))
      {
        /* Variables for storing the width for the left and right part (in pixels). */
        var left_width = 0, right_width = 0;
        var col, left_part, right_part, parts, new_html;
        i++; /* Increase the counter since we are working on a new column. */
        col = $(this);

        /* For the col index + 1 (nth-child starts counting at 1), find the table
         * cells in the current table.
         */
        table.find('> tbody > tr > td:nth-child('+ (index + 1) +')').each(function(index)
        {
          /* Split the html on the specified char. */
          parts = $(this).html().split(col.attr('char'));
          new_html = '';


          /* The first element is always the left part. The remaining part(s) are
           * the right part. Should there be more chars in the string, the right
           * parts are rejoined again with the specified char. 
           */
          left_part = parts.shift();
          right_part = parts.join(',');

          /* Add a left part to the new html if the left part isn't empty*/
          if (!empty(left_part))
          {
            new_html = new_html + '<span class="left">' + left_part + '</span>';
          }

          /* Add the specified char and the right part to the new html if 
           * the right part isn't empty*/
          if (!empty(right_part))
          {
            new_html = new_html + col.attr('char') + '<span class="right">' + right_part + '</span>';
          }

          /* If there is a new html, the width must be determined and a class is
           * added.
           * 
           * Note: outerWidth is used instead of width so padding, margin and
           * borders are taken into account.
           */
          if (!empty(new_html))
          {
            $(this).html(new_html); /* Set the new html. */
            $(this).addClass('char-align-' + i); /* Add a class to the table cell. */

            /* Get the left span to determine its outer width. */
            leftSpan = $(this).children('.left');

            if (!empty(leftSpan) && left_width < leftSpan.outerWidth())
            {
              left_width = leftSpan.outerWidth();
            }

            /* Get the right span to determine its outer width. */
            rightSpan = $(this).children('.right');

            if (!empty(rightSpan) && right_width < rightSpan.outerWidth())
            {
              right_width = rightSpan.outerWidth();
            }

          }

        });

        /* Only if any width is larger then 0, add a style. */
        if (left_width > 0 || right_width > 0)
        {
          style_text = '<style type="text/css">.fix-align-char td.char-align-' + (i) + ' span.left { float: left; text-align: right; width: ' + (left_width) + 'px; }\n.fix-align-char td.char-align-' + (i) + ' span.right { float: right; text-align: left; width: ' + right_width + 'px; }</style>';
          $('head').append(style_text);
        }

      }
    });
  });

}

$(document).ready(function(){
  alignNumbers();
});
Mondane
  • 498
  • 1
  • 5
  • 21