2015 - Development Simply Put

A blog simplifies main concepts in IT development and provides tips, hints, advices and some re-usable code. "If you can't explain it simply, you don't understand it well enough" -Albert Einstein

  • Development Simply Put

    If you can't explain it simply, you don't understand it well enough.

    Read More
  • Integrant

    Based in the U.S. with offshore development centers in Jordan and Egypt, Integrant builds quality custom software since 1992. Our staff becomes an extension of your team to eliminate the risks of software development outsourcing and our processes...

    Read More
  • ITWorx

    ITWorx is a global software professional services organization. Headquartered in Egypt, the company offers Portals, Business Intelligence, Enterprise Application Integration and Application Development Outsourcing services to Global 2000 companies.

    Read More
  • Information Technology Institute

    ITI is a leading national institute established in 1993 by the Information and Decision Support Centre.

    Read More

2015-02-08

Knockout Advanced Tree Library & Control





http://knockoutadvancedtree.byethost10.com


All code samples used on this post can be downloaded from here


This post shows you how to fully implement a tree control using knockout. It was taken into consideration while writing the code to separate the business-related code from the core implementation as far as possible without adding too much complexity to the whole solution.

You can take this library as a base which you can modify to be adapted to your needs. Feel free to do it and for sure if you have any comments or ideas I am all ears.


Supported Features
  1. Flat input data
  2. Dynamic node object properties
  3. Sorting by node object properties
  4. Searching by node object properties
    1. By like patterns (case sensitive/insensitive) or whole words
    2. By regular expressions
    3. Expanding to matching nodes
    4. Highlighting matching nodes
  5. Expand/Collapse
  6. Adding nodes
  7. Extensibility

Note:
The demo page may take quite time to fully load and the tree to appear because I meant to load a quite number of nodes to test the tree under such conditions and not only happy scenarios. The tree in the demo will load 1230 nodes.


Note:
This post is not meant to be a knockout tutorial. It assumes that you have experience with knockout and that's why I am not going to explain every line of code. Most of the code is business irrelative and dependent except for a few lines which will be highlighted with inline comments on the code.

Note:
The tree structure and styling used in this solution is built on the jsTree plugin. jsTree is jquery plugin, that provides interactive trees. It is absolutely free, open source and distributed under the MIT license. jsTree is easily extendable, themable and configurable, it supports HTML & JSON data sources and AJAX loading. It has several great features but in this solution we just used its themes.


Due to the huge amount of code I will not post the code here but you have download the code from this link.
You can also check the live demo on this link.


That's it. Hope you find this useful.


http://knockoutadvancedtree.byethost10.com



2015-02-02

Knockout Datagrid With Sorting, Paging And Searching




www.googledrive.com/host/0BxGPghTC6VdBfmVlRzg3d0xuSW1lVVRReXUwYlZDbFl6dTdhaUdRazJwb2kxNzVDRllVbTg


All code samples used on this post can be downloaded from here.


If you are interested into having a quick recap on the paging concept you can check the post Paging Concept - The Main Equations To Make It Easy first.


This post shows you how to fully implement a data grid with sorting, paging and searching features. It was taken into consideration while writing the code to separate the business-related code from the core implementation as far as possible without adding too much complexity to the whole solution.

You can take the code below as a base which you can modify to be adapted to your needs. Feel free to do it and for sure if you have any comments or ideas I am all ears.


Note:
This post is not meant to be a knockout tutorial. It assumes that you have experience with knockout and that's why I am not going to explain every line of code. Most of the code is business irrelative and dependent except for a few lines which will be highlighted with inline comments on the code.


index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Development Simply Put | DataGrid With Sorting, Paging And Searching</title>
  
  <link rel="stylesheet" type="text/css" href="css/layout.css"/>
  <link type="text/css" rel="stylesheet" href="js/jtable.2.4.0/themes/metro/green/jtable.min.css"/>
  
  <script type="text/javascript" src="js/Vendor/jquery-1.11.0.min.js"></script>
  <script type="text/javascript" src="js/Vendor/jquery-ui-1.9.2.js"></script>
  <script type="text/javascript" src="js/Vendor/jquery.layout-1.3.0.js"></script>
  <script type="text/javascript" src="js/Vendor/jquery.paginate.js"></script>
  <script type="text/javascript" src="js/Vendor/KO/knockout-3.2.0.js"></script>
  <script type="text/javascript" src="js/Vendor/KO/knockout.mapping-latest.js"></script>
  <script type="text/javascript" src="js/DecimalSignedTextBoxWithLimit.js"></script>
  <script type="text/javascript" src="js/Common.js"></script>
  <script type="text/javascript" src="js/CommonViewModelsDefinitions.js"></script>
  <script type="text/javascript" src="js/Employees.js"></script>
    </head>
 
 <body>
  <div id="EmployeesMainDiv"></div>
 </body>
</html>

Employees_Template.html
<div class="jtable-main-container">
 <div class="jtable-title">
  <div class="jtable-title-text">
            Employees
  </div>
     <div class="jtable-toolbar">
   <span class="jtable-toolbar-item jtable-toolbar-item-add-record" style="vertical-align:middle!important;">
    <span class="jtable-toolbar-item-text">
     Highlight  
     <input type="text" style="width:200px;" data-bind="value: $root.SearchText, valueUpdate: 'afterkeydown'" />
    </span>
   </span>
      <!-- ko if:  $root.WithPaging() -->
    <span class="jtable-toolbar-item jtable-toolbar-item-add-record" style="vertical-align:middle!important;">
     <span class="jtable-toolbar-item-text">
      Page Size 
      <input type="text" style="width:50px;" data-bind="value: $root.PageSize, isUnSignedIntegerTextBox: true" />
     </span>
    </span>
   <!-- /ko -->
  </div> 
 </div>
    <table class="jtable">
  <thead>
   <tr>
    <th class="jtable-column-header jtable-column-header-sortable" data-bind="click: function () { $root.SetEmployeesSorting('Id'); }, css: { 'jtable-column-header-sorted-asc': $root.SortingColumn() == 'Id' && $root.SortingDirection() == 'asc', 'jtable-column-header-sorted-desc': $root.SortingColumn() == 'Id' && $root.SortingDirection() != 'asc' }">
     <div class="jtable-column-header-container">
      <span class="jtable-column-header-text">
       Id
      </span>
     </div>
    </th>
    <th class="jtable-column-header jtable-column-header-sortable" data-bind="click: function () { $root.SetEmployeesSorting('Name'); }, css: { 'jtable-column-header-sorted-asc': $root.SortingColumn() == 'Name' && $root.SortingDirection() == 'asc', 'jtable-column-header-sorted-desc': $root.SortingColumn() == 'Name' && $root.SortingDirection() != 'asc' }">
     <div class="jtable-column-header-container">
      <span class="jtable-column-header-text">
       Name
      </span>
     </div>
    </th>
   </tr>
  </thead>
  <tbody id="EmployeesTableBody">
   <!-- ko if:  $data.Employees().length > 0 -->
    <!-- ko template: {name: 'EmployeesTemplate', foreach: $data.Employees, as: 'employee'}-->
    <!-- /ko -->
   <!-- /ko -->
   <!-- ko ifnot:  $data.Employees().length > 0 -->
    <tr class="jtable-data-row jtable-row-odd">
     <td colspan="2">
      No employees found
     </td>
    </tr>
   <!-- /ko -->
  </tbody>
 </table>
    <!-- ko if:  $data.Employees().length > 0 -->
        <div style="display:block;" id="EmployeesPager" class="page_navigation"></div>
    <!-- /ko -->
</div>

<script id="EmployeesTemplate" type="text/html">
    <tr class="jtable-data-row" data-bind="css: {'not-found-on-search': !$root.SearchTextExistsOnRow(employee), 'jtable-row-even': ($index() % 2) == 0, 'jtable-row-odd': ($index() % 2) != 0 }">
        <td>
            <label data-bind="highlightThis: $root.SearchText(), text: employee.Id(), shortenAt: AppSettings.ShortenAt, moreText: AppSettings.ShortenMoreText, lessText: AppSettings.ShortenLessText"></label>
        </td>
  <td>
            <label data-bind="highlightThis: $root.SearchText, text: employee.Name(), shortenAt: AppSettings.ShortenAt, moreText: AppSettings.ShortenMoreText, lessText: AppSettings.ShortenLessText"></label>
        </td>
    </tr>
</script>

CommonViewModelsDefinitions.js
var AppSettings = new Object();
AppSettings.PageSize = 5;
AppSettings.ShortenAt = 15;
AppSettings.ShortenMoreText = "more";
AppSettings.ShortenLessText = "less";


function EmployeesViewModelDefinition(settings) {
    var self = this;

    self.IsNullOrUndefinedOrEmpty = function (obj) {
        return (typeof (obj) == 'undefined' || undefined == obj || null == obj || '' == obj.toString().trim());
    };

    self.finalSettings = {
        WithPaging: (self.IsNullOrUndefinedOrEmpty(settings.WithPaging)) ? false : settings.WithPaging,
        PageSize: (self.IsNullOrUndefinedOrEmpty(settings.PageSize)) ? AppSettings.PageSize : settings.PageSize,
        ActualCurrentPageNumber: (self.IsNullOrUndefinedOrEmpty(settings.ActualCurrentPageNumber)) ? 1 : settings.ActualCurrentPageNumber,
        ActualNumberOfPages: (self.IsNullOrUndefinedOrEmpty(settings.ActualNumberOfPages)) ? 1 : settings.ActualNumberOfPages,
        ActualTotalNumberOfRows: (self.IsNullOrUndefinedOrEmpty(settings.ActualTotalNumberOfRows)) ? 0 : settings.ActualTotalNumberOfRows,
        ActualCurrentPageRowsCount: (self.IsNullOrUndefinedOrEmpty(settings.ActualCurrentPageRowsCount)) ? 0 : settings.ActualCurrentPageRowsCount,
        PagerDomElementId: (self.IsNullOrUndefinedOrEmpty(settings.PagerDomElementId)) ? null : settings.PagerDomElementId,
        SortingColumn: (self.IsNullOrUndefinedOrEmpty(settings.SortingColumn)) ? 'Id' : settings.SortingColumn,
        SortingDirection: (self.IsNullOrUndefinedOrEmpty(settings.SortingDirection)) ? 'asc' : settings.SortingDirection,
        WithPagingUpdatedDelegate: (self.IsNullOrUndefinedOrEmpty(settings.WithPagingUpdatedDelegate)) ? function (currentSettings) { } : settings.WithPagingUpdatedDelegate,
        PageSizeUpdatedDelegate: (self.IsNullOrUndefinedOrEmpty(settings.PageSizeUpdatedDelegate)) ? function (currentSettings) { } : settings.PageSizeUpdatedDelegate,
        ActualCurrentPageNumberUpdatedDelegate: (self.IsNullOrUndefinedOrEmpty(settings.ActualCurrentPageNumberUpdatedDelegate)) ? function (currentSettings) { } : settings.ActualCurrentPageNumberUpdatedDelegate,
        SortingColumnUpdatedDelegate: (self.IsNullOrUndefinedOrEmpty(settings.SortingColumnUpdatedDelegate)) ? function (currentSettings) { } : settings.SortingColumnUpdatedDelegate,
        SortingDirectionUpdatedDelegate: (self.IsNullOrUndefinedOrEmpty(settings.SortingDirectionUpdatedDelegate)) ? function (currentSettings) { } : settings.SortingDirectionUpdatedDelegate
    };
 
    self.GetFinalSettings = function () {
        for (var prop in self) {
            if (self.finalSettings.hasOwnProperty(prop)) {
                self.finalSettings[prop] = self[prop]();
            }
        }

        return self.finalSettings;
    };

    self.SynchronizeSettings = function (source, destination) {
        for (var prop in destination) {
            if (destination.hasOwnProperty(prop)) {
                if (typeof (source[prop]) != 'undefined' && null != source[prop]) {
                    destination[prop] = source[prop];
                }
            }
        }
    };
 
 self.SearchText = ko.observable();
 
    self.private_WithPaging = ko.observable(self.finalSettings.WithPaging);
    self.WithPaging = ko.computed({
        read: function () {
            return self.private_WithPaging();
        },
        write: function (value) {
            self.private_WithPaging(value);

            if (typeof (self.finalSettings.WithPagingUpdatedDelegate) != 'undefined' && null != self.finalSettings.WithPagingUpdatedDelegate) {
                self.finalSettings.WithPagingUpdatedDelegate(self.GetFinalSettings());
            }
        },
        deferEvaluation: true
    });

    self.private_PageSize = ko.observable(self.finalSettings.PageSize);
    self.PageSize = ko.computed({
        read: function () {
            return self.private_PageSize();
        },
        write: function (value) {
            if (typeof (value) == 'undefined' || null == value || '' == value) {
                value = AppSettings.PageSize;
            }

            self.private_PageSize(value);

            if (typeof (self.finalSettings.PageSizeUpdatedDelegate) != 'undefined' && null != self.finalSettings.PageSizeUpdatedDelegate) {
                self.finalSettings.PageSizeUpdatedDelegate(self.GetFinalSettings());
            }
        },
        deferEvaluation: true
    });

    self.private_PagerDomElementId = ko.observable(self.finalSettings.PagerDomElementId);
    self.PagerDomElementId = ko.computed({
        read: function () {
            return self.private_PagerDomElementId();
        },
        write: function (value) {
            self.private_PagerDomElementId(value);
        },
        deferEvaluation: true
    });

    self.private_SortingColumn = ko.observable(self.finalSettings.SortingColumn);
    self.SortingColumn = ko.computed({
        read: function () {
            return self.private_SortingColumn();
        },
        write: function (value) {
            if (value == self.private_SortingColumn()) {
                self.private_SortingDirection(self.ToggleSortingDirection(self.SortingDirection()));
            }
            else {
                self.private_SortingColumn(value);
            }

            if (!self.WithPaging()) {
                self.SortEmployees();
            }
            else if (typeof (self.finalSettings.SortingColumnUpdatedDelegate) != 'undefined' && null != self.finalSettings.SortingColumnUpdatedDelegate) {
                self.finalSettings.SortingColumnUpdatedDelegate(self.GetFinalSettings());
            }
        },
        deferEvaluation: true
    });

    self.ToggleSortingDirection = function (currentDirection) {
        var finalDirection;

        if (currentDirection.toLowerCase().indexOf('asc') > -1) {
            finalDirection = 'desc';
        }
        else {
            finalDirection = 'asc';
        }

        return finalDirection;
    };

    self.private_SortingDirection = ko.observable(self.finalSettings.SortingDirection);
    self.SortingDirection = ko.computed({
        read: function () {
            return self.private_SortingDirection();
        },
        write: function (value) {
            self.private_SortingDirection(self.ToggleSortingDirection(self.SortingDirection()));

            if (!self.WithPaging()) {
                self.SortEmployees();
            }
            else if (typeof (self.finalSettings.SortingDirectionUpdatedDelegate) != 'undefined' && null != self.finalSettings.SortingDirectionUpdatedDelegate) {
                self.finalSettings.SortingDirectionUpdatedDelegate(self.GetFinalSettings());
            }
        },
        deferEvaluation: true
    });

    self.SetEmployeesSorting = function (sortingColumn) {
        self.SortingColumn(sortingColumn);
    };

    self.SortEmployees = function () {
        self.Employees.sort(self.SortEmployeesComparer);
    };

    self.SortEmployeesComparer = function (a, b) {
        var result = 0;

        var sortingColumn = self.SortingColumn();
        var sortingDirection = self.SortingDirection();

        var valA = a[sortingColumn]();
        var valB = b[sortingColumn]();

        if (isNumeric(valA) && isNumeric(valB)) {
            valA = valA.toString().toLowerCase().replace(',', '');
            valB = valB.toString().toLowerCase().replace(',', '');

            if (parseFloat(valA) == parseFloat(valB)) {
                result = 0;
            }
            else if (parseFloat(valA) > parseFloat(valB)) {
                result = 1;
            }
            else {
                result = -1;
            }
        }
        else {
            if (valA == valB) {
                result = 0;
            }
            else if (valA > valB) {
                result = 1;
            }
            else {
                result = -1;
            }
        }

        if (sortingDirection.toLowerCase().indexOf('asc') == -1) {
            result = result * -1;
        }

        return result;
    };

    self.DataBind = function (JSEmployees, actualNumberOfPages, actualTotalNumberOfRows, actualCurrentPageNumber, actualCurrentPageRowsCount) {
        var retrievedEmployees = ko.mapping.fromJS(JSEmployees);

        self.Employees.removeAll();

        for (var i = 0; i < retrievedEmployees().length; i++) {
            var Employee = (retrievedEmployees())[i];
            self.Employees.push(Employee);
        }

        self.finalSettings.ActualNumberOfPages = actualNumberOfPages;
        self.finalSettings.ActualTotalNumberOfRows = actualTotalNumberOfRows;
        self.finalSettings.ActualCurrentPageNumber = actualCurrentPageNumber
        self.finalSettings.ActualCurrentPageRowsCount = actualCurrentPageRowsCount;
        self.BuildPager();
    };

    self.DataBindFromSettings = function (JSEmployees, newSettings) {
        var finalSettings = self.GetFinalSettings();
        self.SynchronizeSettings(newSettings, finalSettings);
        self.DataBind(JSEmployees, finalSettings.ActualNumberOfPages, finalSettings.ActualTotalNumberOfRows, finalSettings.ActualCurrentPageNumber, finalSettings.ActualCurrentPageRowsCount);
    };

    self.BuildPager = function () {
        if (!self.IsNullOrUndefinedOrEmpty(self.PagerDomElementId()) && self.WithPaging() && self.Employees().length > 0 && self.finalSettings.ActualNumberOfPages > 1) {
            $("#" + self.PagerDomElementId()).show();

            $("#" + self.PagerDomElementId()).paginate({
                count: self.finalSettings.ActualNumberOfPages,
                start: self.finalSettings.ActualCurrentPageNumber,
                display: 11,
                border: false,
                text_color: 'rgba(255, 255, 255, 1)',
                background_color: 'rgba(255, 255, 255, 0)',
                text_hover_color: '#FB6E52',
                background_hover_color: '#FFFFFF',
                images: false,
                onChange: function (newPageNumber) {
                    if (newPageNumber != self.finalSettings.ActualCurrentPageNumber) {
                        self.finalSettings.ActualCurrentPageNumber = newPageNumber;

                        if (typeof (self.finalSettings.ActualCurrentPageNumberUpdatedDelegate) != 'undefined' && null != self.finalSettings.ActualCurrentPageNumberUpdatedDelegate) {
                            self.finalSettings.ActualCurrentPageNumberUpdatedDelegate(self.GetFinalSettings());
                        }
                    }
                }
            });
        }
        else {
            $("#" + self.PagerDomElementId()).hide();
        }
    };
 
 
 //Define your own entities array here
 self.Employees = ko.observableArray();
 
 //Include your own entities columns here
 self.SearchTextExistsOnRow = function(employee) {
  if(self.IsNullOrUndefinedOrEmpty(self.SearchText())) {
   return true;
  }
  else {
   return (employee.Id().toString().toLowerCase().indexOf(self.SearchText().toLowerCase())) != -1
   || (employee.Name().toString().toLowerCase().indexOf(self.SearchText().toLowerCase())) != -1;
  }  
 };
}

Common.js
jQuery.fn.highlight = function (str, className) {
    var regex = new RegExp(str, "gi");
    return this.each(function () {
        $(this).contents().filter(function() {
            return this.nodeType == 3 && regex.test(this.nodeValue);
        }).replaceWith(function() {
            return (this.nodeValue || "").replace(regex, function(match) {
                return "<span class=\"" + className + "\">" + match + "</span>";
            });
        });
    });
};

function isBound(elemenId) {
    var result = false;
    var x = ko.dataFor($("#" + elemenId)[0]);

    if (typeof (x) != 'undefined' & null != x) {
        result = true;
    }

    return result;
};

function IsNullOrUndefinedOrEmpty(obj) {
    return (typeof (obj) == 'undefined' || undefined == obj || null == obj || '' == obj);
}

function isNumber(evt) {
    evt = (evt) ? evt : window.event;
    var charCode = (evt.which) ? evt.which : evt.keyCode;
    if (charCode > 31 && (charCode < 48 || charCode > 57)) {
        return false;
    }
    return true;
}

function numberWithCommas(x) {
    var parts = x.toString().split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    return parts.join(".");
}

function getMaxOccurance(array) {
    if (array.length == 0)
        return null;
    var modeMap = {};
    var maxEl = array[0], maxCount = 1;
    for (var i = 0; i < array.length; i++) {
        var el = array[i];
        if (modeMap[el] == null)
            modeMap[el] = 1;
        else
            modeMap[el]++;
        if (modeMap[el] > maxCount) {
            maxEl = el;
            maxCount = modeMap[el];
        }
    }
    return maxEl;
}

function isNormalInteger(str) {
    var n = ~~Number(str);
    return String(n) === str && n > 0;
}

/*-----------------------------------KO Extenders-------------------------------*/
ko.bindingHandlers.isDecimalSignedTextBoxWithLimit = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);

        // Grab some more data from another binding property
        var noOfDigitsBeforeDecimal = allBindings.get('noOfDigitsBeforeDecimal') || 10;
        var noOfDigitsAfterDecimal = allBindings.get('noOfDigitsAfterDecimal') || 10;
        var defaultValue = allBindings.get('defaultValue') || 0;
        var invalidInputCallack = allBindings.get('invalidInputCallack') || null;

        // Now manipulate the DOM element
        if (valueUnwrapped == true) {
            $(element).on("keypress", function () {
                return checkSignedDecimalWithLimit(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal);
            });

            $(element).on("keyup", function () {
                checkPasteSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, false);
            });

            $(element).on("change", function () {
                checkPasteSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, false);
            });

            $(element).on("blur", function () {
                checkPasteSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, true);
            });
        }
    },
    init: function (element, valueAccessor, allBindings) {
        var noOfDigitsBeforeDecimal = allBindings.get('noOfDigitsBeforeDecimal') || 10; // 10 is default duration unless otherwise specified
        var noOfDigitsAfterDecimal = allBindings.get('noOfDigitsAfterDecimal') || 10; // 10 is default duration unless otherwise specified

        adjustZerosAfterDecimalPoint(element, noOfDigitsAfterDecimal);
        adjustNumbersBeforeDecimalPoint(element, noOfDigitsBeforeDecimal);
    }
};

ko.bindingHandlers.isDecimalUnSignedTextBoxWithLimit = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);

        // Grab some more data from another binding property
        var noOfDigitsBeforeDecimal = allBindings.get('noOfDigitsBeforeDecimal') || 10;
        var noOfDigitsAfterDecimal = allBindings.get('noOfDigitsAfterDecimal') || 10;
        var defaultValue = allBindings.get('defaultValue') || 0;
        var invalidInputCallack = allBindings.get('invalidInputCallack') || null;

        // Now manipulate the DOM element
        if (valueUnwrapped == true) {
            $(element).on("keypress", function () {
                return checkUnSignedDecimalWithLimit(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal);
            });

            $(element).on("keyup", function () {
                checkPasteUnSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, false);
            });

            $(element).on("change", function () {
                checkPasteUnSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, false);
            });

            $(element).on("blur", function () {
                checkPasteUnSigned(this, noOfDigitsBeforeDecimal, noOfDigitsAfterDecimal, defaultValue, function (elem) { invalidInputCallack(elem); }, true);
            });

            $(element).on("focus", function () {
                var value = $(this).val();
                var formattedDefaultValue = formatZerosAfterDecimalPoint(defaultValue.toString(), noOfDigitsAfterDecimal);
                formattedDefaultValue = formatNumbersBeforeDecimalPoint(formattedDefaultValue, noOfDigitsBeforeDecimal);

                if (value == formattedDefaultValue) {
                    $(this).val('');
                }
            });
        }
    },
    init: function (element, valueAccessor, allBindings) {
        var noOfDigitsBeforeDecimal = allBindings.get('noOfDigitsBeforeDecimal') || 10; // 10 is default duration unless otherwise specified
        var noOfDigitsAfterDecimal = allBindings.get('noOfDigitsAfterDecimal') || 10; // 10 is default duration unless otherwise specified

        adjustZerosAfterDecimalPoint(element, noOfDigitsAfterDecimal);
        adjustNumbersBeforeDecimalPoint(element, noOfDigitsBeforeDecimal);
    }
};

ko.bindingHandlers.isUnSignedIntegerTextBox = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);

        // Grab some more data from another binding property
        var defaultValue = allBindings.get('defaultValue') || 0;

        // Now manipulate the DOM element
        if (valueUnwrapped == true) {
            $(element).on("keypress", function (event) {
                return isNumber(event)
            });

            $(element).bind("paste", function (e) {
                e.preventDefault();
            });

            $(element).on("focus", function () {
                var value = $(this).val();

                if (value == defaultValue) {
                    $(this).val('');
                }
            });

            $(element).on("blur", function () {
                var value = $(this).val();

                if (null == value || '' == value) {
                    $(this).val(defaultValue);
                }
            });
        }
    }
};

ko.bindingHandlers.shortenAt = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);

        // Grab some more data from another binding property
        var moreText = allBindings.get('moreText') || "more";
        var lessText = allBindings.get('lessText') || "less";

        // Now manipulate the DOM element
        if (valueUnwrapped > 0) {
            var content = $(element).text();
            if (content.length > valueUnwrapped) {
                var con = content.substr(0, valueUnwrapped);
                var hcon = content.substr(valueUnwrapped, content.length - valueUnwrapped);
                var txt = con + '<span class="dots">...</span><span class="morecontent"><span>' + hcon + '</span>&nbsp;&nbsp;<a href="" class="moretxt">' + moreText + '</a></span>';
                $(element).html(txt);

                $(element).find(".moretxt").click(function () {
                    if ($(this).hasClass("sample")) {
                        $(this).removeClass("sample");
                        $(this).text(moreText);
                    } else {
                        $(this).addClass("sample");
                        $(this).text(lessText);
                    }
                    $(this).parent().prev().toggle();
                    $(this).prev().toggle();
                    return false;
                });
            }
        }
    }
};

ko.bindingHandlers.highlightThis = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.utils.unwrapObservable(value);

        // Grab some more data from another binding property
        //var moreText = allBindings.get('moreText') || "more";
        //var lessText = allBindings.get('lessText') || "less";

        // Now manipulate the DOM element
        if (typeof(valueUnwrapped) != 'undefined' && null != valueUnwrapped && valueUnwrapped.toString() != '') {
   if (!$(element).is("[backup]")) {
    $(element).attr("backup", $(element).html());
   }
   
   var backup = $(element).attr("backup");
   $(element).html(backup);
   $(element).highlight(valueUnwrapped, "highlighted-search-text");
        }
  else {
   var backupAttr = $(this).attr('backup');

   if ($(element).is("[backup]")) {debugger;
    var backup = $(element).attr("backup");
    $(element).html(backup);
   }
  }
    }
};
/*-----------------------------------KO Extenders-------------------------------*/

Employees.js
var EmployeesViewModel = null;
 
function ResetEmployeesView() {
    EmployeesViewModel = new EmployeesViewModelDefinition({
        SortingColumnUpdatedDelegate: GetEmployeesPagedOrdered,
        SortingDirectionUpdatedDelegate: GetEmployeesPagedOrdered,
        ActualCurrentPageNumberUpdatedDelegate: GetEmployeesPagedOrdered,
        PageSizeUpdatedDelegate: GetEmployeesPagedOrdered,
        WithPaging: true,
        PagerDomElementId: 'EmployeesPager'
    });
}

function GetEmployeesPagedOrdered(currentSettings, callback) {
 EmployeesViewModel.SearchText("");

    var pageNumber = currentSettings.ActualCurrentPageNumber;
 var pageSize = currentSettings.PageSize;
 var orderByPropertyName = currentSettings.SortingColumn;
 var sortingDirection = currentSettings.SortingDirection;
 
 var token = GetEmployeesPagedOrderedByAjax(pageSize, pageNumber, orderByPropertyName, sortingDirection);
 
 if(null != token) {
  var employees = token.Employees;
  var actualNumberOfPages = token.ActualNumberOfPages;
  var actualTotalNumberOfRows = token.ActualTotalNumberOfRows;
  var actualCurrentPageNumber = token.ActualCurrentPageNumber;
  var actualCurrentPageRowsCount = token.ActualCurrentPageRowsCount;

  var newSettings = {
   ActualNumberOfPages: actualNumberOfPages,
   ActualTotalNumberOfRows: actualTotalNumberOfRows,
   ActualCurrentPageNumber: actualCurrentPageNumber,
   ActualCurrentPageRowsCount: actualCurrentPageRowsCount
  };

  $("#EmployeesMainDiv").load('KOTemplates/Employees_Template.html',
   function () {
    ko.cleanNode($("#EmployeesMainDiv")[0]);
    if (!isBound('EmployeesMainDiv')) {
     ko.applyBindings(EmployeesViewModel, $("#EmployeesMainDiv")[0]);
    }

    EmployeesViewModel.DataBindFromSettings(employees, newSettings);
    
    if (typeof (callback) != 'undefined' && null != callback) {
     callback();
    }
   }
  );
 }
};

//This is the method resonsible for retrieving employees from the back-end by sorting and paging.
//The implementation of this method differs from one system to another depending on the back-end and the server-side framework and language.
//The most important point to notice here is the "Paging Calculations" section in the method below.
//This section should be taken into consideration and implemented. 
function GetEmployeesPagedOrderedByAjax(pageSize, pageNumber, orderByPropertyName, sortingDirection) {
 var resultToken = null;
 
 var AllEmployees = new Array();
 AllEmployees.push({Id:1, Name:"Employee 1"});
 AllEmployees.push({Id:2, Name:"Employee 2"});
 AllEmployees.push({Id:3, Name:"Employee 3"});
 AllEmployees.push({Id:4, Name:"Employee 4"});
 AllEmployees.push({Id:5, Name:"Employee 5"});
 AllEmployees.push({Id:6, Name:"Employee 6"});
 AllEmployees.push({Id:7, Name:"Employee 7"});
 AllEmployees.push({Id:8, Name:"Employee 8"});
 AllEmployees.push({Id:9, Name:"Employee 9"});
 AllEmployees.push({Id:10, Name:"Employee 10"});
 AllEmployees.push({Id:11, Name:"Employee 11"});
 AllEmployees.push({Id:12, Name:"Employee 12"});
 AllEmployees.push({Id:13, Name:"Employee 13"});
 AllEmployees.push({Id:14, Name:"Employee 14"});
 AllEmployees.push({Id:15, Name:"Employee 15"});
 AllEmployees.push({Id:16, Name:"Employee 16"});
 AllEmployees.push({Id:17, Name:"Employee 17"});
 AllEmployees.push({Id:18, Name:"Employee 18"});
 
 if(AllEmployees.length > 0) {
  AllEmployees.sort(function (a, b) {
   var result = 0;
   var valA = a[orderByPropertyName];
   var valB = b[orderByPropertyName];
   
   if (isNumeric(valA) && isNumeric(valB)) {
    valA = valA.toString().toLowerCase().replace(',', '');
    valB = valB.toString().toLowerCase().replace(',', '');

    if (parseFloat(valA) == parseFloat(valB)) {
     result = 0;
    }
    else if (parseFloat(valA) > parseFloat(valB)) {
     result = 1;
    }
    else {
     result = -1;
    }
   }
   else {
    if (valA == valB) {
     result = 0;
    }
    else if (valA > valB) {
     result = 1;
    }
    else {
     result = -1;
    }
   }

   if (sortingDirection.toLowerCase().indexOf('asc') == -1) {
    result = result * -1;
   }

   return result;
  });
  
  //-------------------------------------------------Paging Calculations-------------------------------------------------
  if(pageSize <= 0) {
   pageSize = AllEmployees.length;
  }
  
  var maxNumberOfPages = Math.max(1, Math.ceil(parseFloat(parseFloat(AllEmployees.length) / parseFloat(pageSize))));
  
  if(pageNumber < 1) {
   pageNumber = 1;
  }
  else if(pageNumber > maxNumberOfPages) {
   pageNumber = maxNumberOfPages;
  }
  
  var pageIndex = pageNumber - 1;
  var firstItemIndex = pageIndex * pageSize;
  var lastItemIndex = (pageIndex * pageSize) + (pageSize - 1);
  var actualNumberOfPages = maxNumberOfPages;
  var actualTotalNumberOfRows = AllEmployees.length;
  var actualCurrentPageNumber = pageNumber;
  
  if(lastItemIndex > AllEmployees.length) {
   lastItemIndex = AllEmployees.length - 1;
  }
  
  var actualCurrentPageRowsCount = lastItemIndex - firstItemIndex;
  //-------------------------------------------------Paging Calculations-------------------------------------------------
  
  resultToken = new Object();
  resultToken.Employees = AllEmployees.slice(firstItemIndex, lastItemIndex + 1);
  resultToken.ActualNumberOfPages = actualNumberOfPages;
  resultToken.ActualTotalNumberOfRows = actualTotalNumberOfRows;
  resultToken.ActualCurrentPageNumber = actualCurrentPageNumber;
  resultToken.ActualCurrentPageRowsCount = actualCurrentPageRowsCount;
 }
 
 return resultToken;
}

function BindGrid(callback) {
    GetEmployeesPagedOrdered(EmployeesViewModel.GetFinalSettings(), callback);
};

$(document).ready(function(){
 ResetEmployeesView();
 BindGrid();
});

Layout.css
body {
    font-family: Arial, Helvetica, sans-serif;
    background: grey no-repeat center center;
    min-width: 980px;
    font-size: 12px;
    line-height: normal;
    color: black;
}

* {
    padding: 0;
    outline: none;
    border: none;
    text-align: left;
    list-style: none;
}

.not-found-on-search {
 display: none!important;
}

.highlighted-search-text {
 background-color: #ff2;
}

table thead tr th, table tbody tr td {
    vertical-align: top;
}

a, a:hover, a:visited {
    text-decoration: none;
    outline: none;
    cursor: pointer;
}

a:hover, a:focus, a:active {
 color: black /*c4c4c4*/;
 text-decoration: none;
}

::selection {
    background: #666;
    color: black;
    text-shadow: none;
}

html, body {
    height: 100%;
}

.jPaginate {
    float: right;
    height: 34px;
    position: relative;
    color: #a5a5a5;
    font-size: small;
    width: auto;
    padding-right: 65px;
    margin-top: 10px;
}

.jPaginate a {
 line-height: 15px;
 height: 18px;
 cursor: pointer;
 padding: 2px 5px;
 margin: 2px;
 float: left;
}

.jPag-control-back {
    position: absolute;
    left: 0px;
}

.jPag-control-front {
    position: absolute;
    top: 0px;
}

.jPaginate span {
    cursor: pointer;
}

ul.jPag-pages {
    float: left;
    list-style-type: none;
    margin: 0px 0px 0px 0px;
    padding: 0px;
}

ul.jPag-pages li {
 display: inline;
 float: left;
 padding: 0px;
 margin: 0px;
 font-weight: bold;
}

ul.jPag-pages li a {
 float: left;
 padding: 2px 5px;
}

span.jPag-current {
    cursor: default;
    background-color: rgba(251, 235, 235, 0.38);
    line-height: 15px;
    height: 18px;
    padding: 2px 5px;
    margin: 2px;
    float: left;
}

table tr.jtable-row-odd td, table tr.jtable-data-row td {
    border-bottom: 1px solid #ddd;
    border-right: 1px solid #ddd;
    line-height: 30px;
}


Note:
The table structure and styling used in this solution is built on the jTable plugin. jTable is a jQuery plugin that is used to create AJAX based CRUD tables without coding HTML or Javascript. It has several great features but in this solution we just used its themes. In this solution we used the green metro theme but it provides other themes which you can choose from.



That's it. Again, all code samples used on this post can be downloaded from here.
Hope you find this usefull.



2015-01-30

A Guide For Dealing With Hierarchical, Parent-Child And Tree Form Data Operations



Lately I have been working on more than one project dealing with hierarchical data structures. This encouraged me to try to sum up my experience on this type of structures and corresponding operations.

From time to time I will revisit this post to update it with whatever new I found related to this topic. Please find the assembled posts below and let me know if you have any comments.


How To Apply Recursive SQL Selections On Hierarchical Data
Sometimes we work on systems where a hierarchical data structure exists on some entities like employees and their managers. Both employees and managers can be called employees but there is a self join relation between them as each employee must have a manager. We all faced the situation when we need to get the info about each employee and his/her direct manager. At this point we used to join between the employees (child) table and itself (parent) on the condition that the parent id of the child is equal to the id of the parent. This is good. But, what about if we need to get the hierarchical tree of managers of a certain employee not just his direct manager. It seems as we just need to the same join but more than one time till we are up all the way to the head manager. This is somehow logical but how can we do this number of joins and we don't know the number of levels up to the head manager?!!! If you want to know more, you can read this article.


How To Transform Unsorted Flat Hierarchical Data Structures Into Nested Parent-Child Or Tree Form Objects
Sometimes you have hierarchical data structure presented into an SQL database table where there is a parent-child relation between rows. You may need to transform this flat hierarchical data structure into a parent-child or tree form entity so that you can use it in more advanced or complex business like binding to a tree control or whatever. If you want to know how to do this transformation process, you can read this article.


How To Copy SQL Hierarchical Data At Run-time While Keeping Valid Internal References And Self Joins
Sometimes when you deal with hierarchical data structures you may need to perform internal copy operations. These copy operations should be handled in a smart way as just using INSERT-SELECT statements will mess up the internal references and self joins of the newly inserted records. If you want to know more about this, you can read this article.


For SQL Hierarchical Data, How To Show Only Tree Branches Including Certain Type Of Entities And Discard Others
If you have a tree of entities represented into a SQL database into parent-child relation, you may need to be able to view only the tree branches which include a certain type of entities and discard the branches which don't include this type. If you ever faced such situation or even curious to know how to manage such situation, you can read this article.


Knockout Advanced Tree Library & Control
This post shows you how to fully implement a tree control using knockout. This tree control has great features like; 01.Flat input data, 02.Dynamic node object properties, 03.Sorting by node object properties, 04.Searching by node object properties, 05.Searching by like patterns (case sensitive/insensitive) or whole words, 06.Searching by regular expressions, 07.Expanding to matching nodes, 08.Highlighting matching nodes, 09.Expand/Collapse, 10.Adding nodes, 11.Extensibility. It was taken into consideration while writing the code to separate the business-related code from the core implementation as far as possible without adding too much complexity to the whole solution. You can take this library as a base which you can modify to be adapted to your needs. Feel free to do it and for sure if you have any comments or ideas I am all ears.


That's it. Hope this will help you someday.


2015-01-23

Splitting Daytime Into Chunks To Enhance SQL Bulk Time-based Operations Performance





The best way to understand what this post is about is to start with a real scenario.

One of my colleagues was building a system which controls some motors using some readings coming from electronic sensors. The problem was that the frequency of the sensors readings was so high that the system had to insert a few reading records into the SQL database in few milliseconds. This means that in just a second the system had to insert hundreds or even thousands of records into the database.

This for sure was a big challenge and due to some customer needs and business requirements the approved solution was to compress the sensor readings per a fixed time range at the end of the day. This means that at the end of each day the sensor readings should be divided into groups where each group is limited by a time range of half an hour for example and then the readings of each group should be aggregated by taking the average. This way we will end up having hundreds of readings records into the database instead of thousands or even more. Till now everything is somehow good and logical.

A new challenge aroused while working on the SQL routine which will carry out the bulk aggregation process. The performance of this process was not promising. The process used to take a huge amount of time due to the heavy date-time comparisons and groupings. This was what encouraged me to jump in and try to help.

Before applying any changes the process was going like this; getting all sensor readings which were created between 00:00:00 and 00:15:00 and then taking the average for these readings, then, getting all sensor readings which were created between 00:15:00 and 00:30:00 and then taking the average for these readings, and so on......

As you can see this amount of processing and grouping based on time is huge and this was causing the whole process to be a nightmare performance wise.

One of the best ways to enhance the performance of bulk actions is to try to break the whole one big process into small controllable ones. Then, observe each small process and check whether it, for one record, depends only on the record itself and doesn't need to know anything about other records or not. If this happens, then this small process could be held on single record basis at early stage which could be the moment the record was created or something else based on the business requirements. This will help split the huge processing effort on different moments and stages of the system business life cycle and workflow which will make it more controllable and less recognizable.

Now, to apply the same concept on the problem we have on hand right now we should notice that:
  1. The chunks of time ranges (15 minutes) are the same for all readings and they do not change for any reason unless requested by system user which is done manually or through user interference
  2. The creation date of each sensor reading record is known at the moment the record is created
  3. Most of the processing effort is consumed on the date-time comparisons followed by the groupings
This helped me to decide that:
  1. The chunks of time ranges should be defined only once at the system launch and the results should be kept physically in a table to be used as a quick cached reference
  2. The time chunk to which each sensor reading record belongs should be decided at the moment the record is created
  3. This way each record can have an id of the time chunk it belongs to which will cause the grouping process to be much more easier and effortless


Create Database
USE [master]
GO

CREATE DATABASE [DayChunks] ON  PRIMARY 
( NAME = N'DayChunks', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\DayChunks.mdf' , SIZE = 4096KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'DayChunks_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\DayChunks_log.ldf' , SIZE = 1280KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO

ALTER DATABASE [DayChunks] SET COMPATIBILITY_LEVEL = 100
GO

IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [DayChunks].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO

ALTER DATABASE [DayChunks] SET ANSI_NULL_DEFAULT OFF 
GO

ALTER DATABASE [DayChunks] SET ANSI_NULLS OFF 
GO

ALTER DATABASE [DayChunks] SET ANSI_PADDING OFF 
GO

ALTER DATABASE [DayChunks] SET ANSI_WARNINGS OFF 
GO

ALTER DATABASE [DayChunks] SET ARITHABORT OFF 
GO

ALTER DATABASE [DayChunks] SET AUTO_CLOSE OFF 
GO

ALTER DATABASE [DayChunks] SET AUTO_CREATE_STATISTICS ON 
GO

ALTER DATABASE [DayChunks] SET AUTO_SHRINK OFF 
GO

ALTER DATABASE [DayChunks] SET AUTO_UPDATE_STATISTICS ON 
GO

ALTER DATABASE [DayChunks] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO

ALTER DATABASE [DayChunks] SET CURSOR_DEFAULT  GLOBAL 
GO

ALTER DATABASE [DayChunks] SET CONCAT_NULL_YIELDS_NULL OFF 
GO

ALTER DATABASE [DayChunks] SET NUMERIC_ROUNDABORT OFF 
GO

ALTER DATABASE [DayChunks] SET QUOTED_IDENTIFIER OFF 
GO

ALTER DATABASE [DayChunks] SET RECURSIVE_TRIGGERS OFF 
GO

ALTER DATABASE [DayChunks] SET  DISABLE_BROKER 
GO

ALTER DATABASE [DayChunks] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO

ALTER DATABASE [DayChunks] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO

ALTER DATABASE [DayChunks] SET TRUSTWORTHY OFF 
GO

ALTER DATABASE [DayChunks] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO

ALTER DATABASE [DayChunks] SET PARAMETERIZATION SIMPLE 
GO

ALTER DATABASE [DayChunks] SET READ_COMMITTED_SNAPSHOT OFF 
GO

ALTER DATABASE [DayChunks] SET HONOR_BROKER_PRIORITY OFF 
GO

ALTER DATABASE [DayChunks] SET  READ_WRITE 
GO

ALTER DATABASE [DayChunks] SET RECOVERY FULL 
GO

ALTER DATABASE [DayChunks] SET  MULTI_USER 
GO

ALTER DATABASE [DayChunks] SET PAGE_VERIFY CHECKSUM  
GO

ALTER DATABASE [DayChunks] SET DB_CHAINING OFF 
GO


Create Tables
USE [DayChunks]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Settings](
 [ID] [int] IDENTITY(1,1) NOT NULL,
 [Name] [nvarchar](max) NOT NULL,
 [Value] [nvarchar](max) NULL,
 CONSTRAINT [PK_Settings] PRIMARY KEY CLUSTERED 
(
 [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO





CREATE TABLE [dbo].[DayChunks](
 [ID] [int] IDENTITY(1,1) NOT NULL,
 [ChunkNumOfMinutes] [int] NOT NULL,
 [ChunkNumOfSeconds] [int] NOT NULL,
 [ChunkStart] [time](0) NOT NULL,
 [ChunkEnd] [time](0) NOT NULL,
 [IsLastChunk] [bit] NOT NULL,
 CONSTRAINT [PK_DayChunks] PRIMARY KEY CLUSTERED 
(
 [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[DayChunks] ADD  CONSTRAINT [DF_DayChunks_IsLastChunk]  DEFAULT (0) FOR [IsLastChunk]
GO




CREATE TABLE [dbo].[SensorReadings](
 [ID] [int] IDENTITY(1,1) NOT NULL,
 [CreationDateTime] [datetime] NOT NULL,
 [Reading] [float] NOT NULL,
 [DayChunkID] [int] NULL,
 [DayDate] [date] NOT NULL,
 CONSTRAINT [PK_SensorReadings] PRIMARY KEY CLUSTERED 
(
 [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[SensorReadings] ADD  CONSTRAINT [DF_SensorReadings_CreationDateTime]  DEFAULT (getdate()) FOR [CreationDateTime]
GO

ALTER TABLE [dbo].[SensorReadings] ADD  CONSTRAINT [DF_SensorReadings_DayDate]  DEFAULT (getdate()) FOR [DayDate]
GO





CREATE TABLE [dbo].[AggregatedSensorReadings](
 [ID] [int] IDENTITY(1,1) NOT NULL,
 [DayChunkID] [int] NULL,
 [Reading] [float] NOT NULL,
 [DayDate] [date] NOT NULL,
 CONSTRAINT [PK_AggregatedSensorReadings] PRIMARY KEY CLUSTERED 
(
 [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Create Routines
USE [DayChunks]
GO


SET ANSI_NULLS ON
GO

IF EXISTS
(
 SELECT * FROM dbo.sysobjects
 WHERE id = object_id(N'[dbo].[SetDayChunksSettings]')
 AND OBJECTPROPERTY(id, N'IsProcedure') = 1
)
 
DROP PROCEDURE [dbo].[SetDayChunksSettings]

GO
CREATE PROCEDURE [dbo].[SetDayChunksSettings]
(
    @ChunksNumOfMinutes INT
 , @ChunksNumOfSeconds INT
 , @ChunksStartTime TIME(0)
)
AS
BEGIN
 DELETE FROM Settings
 WHERE Name IN
 (
  'DayChunksStartTime'
  , 'DayChunksNumOfMinutes'
  , 'DayChunksNumOfSeconds'
 )
 
 INSERT INTO Settings
 (
  Name
  , Value
 )
 VALUES
 (
  'DayChunksStartTime'
  , CAST(@ChunksStartTime AS NVARCHAR(MAX))
 )
 ,
 (
  'DayChunksNumOfMinutes'
  , CAST(@ChunksNumOfMinutes AS NVARCHAR(MAX))
 )
 ,
 (
  'DayChunksNumOfSeconds'
  , CAST(@ChunksNumOfSeconds AS NVARCHAR(MAX))
 )
END
GO





IF EXISTS
(
 SELECT * FROM dbo.sysobjects
 WHERE id = object_id(N'[dbo].[CreateDayChunksBasedOnInput]')
 AND OBJECTPROPERTY(id, N'IsProcedure') = 1
)
 
DROP PROCEDURE [dbo].[CreateDayChunksBasedOnInput]

GO
CREATE PROCEDURE [dbo].[CreateDayChunksBasedOnInput]
(
    @ChunksNumOfMinutes INT
 , @ChunksNumOfSeconds INT
 , @ChunksStartTime TIME(0)
)
AS
BEGIN
 DECLARE @MaxEnd TIME(0) = DATEADD(minute, (-1 * @ChunksNumOfMinutes), @ChunksStartTime)
 SET @MaxEnd = DATEADD(second, (-1 * @ChunksNumOfSeconds), @MaxEnd)

 DECLARE @ChunkStart TIME(0) = @ChunksStartTime

 DECLARE @ChunkEnd TIME(0) = DATEADD(minute, @ChunksNumOfMinutes, @ChunksStartTime)
 SET @ChunkEnd = DATEADD(second, @ChunksNumOfSeconds, @ChunkEnd)
 
 TRUNCATE TABLE DayChunks
 
 WHILE (@ChunkEnd < @MaxEnd)
 BEGIN
  INSERT INTO DayChunks
  (
   ChunkNumOfMinutes
   , ChunkNumOfSeconds
   , ChunkStart
   , ChunkEnd
  )
  VALUES
  (
   @ChunksNumOfMinutes
   , @ChunksNumOfSeconds
   , @ChunkStart
   , @ChunkEnd
  )
     
  SET @ChunkStart = DATEADD(minute, @ChunksNumOfMinutes, @ChunkStart)
  SET @ChunkStart = DATEADD(second, @ChunksNumOfSeconds, @ChunkStart)
     
  SET @ChunkEnd = DATEADD(minute, @ChunksNumOfMinutes, @ChunkEnd)
  SET @ChunkEnd = DATEADD(second, @ChunksNumOfSeconds, @ChunkEnd)
 END

 IF(@ChunkEnd = @MaxEnd OR @ChunkEnd > @MaxEnd)
 BEGIN
  IF(@ChunkEnd > @MaxEnd)
  BEGIN
   SET @ChunkEnd = @MaxEnd
  END
  
  INSERT INTO DayChunks
  (
   ChunkNumOfMinutes
   , ChunkNumOfSeconds
   , ChunkStart
   , ChunkEnd
  )
  VALUES
  (
   @ChunksNumOfMinutes
   , @ChunksNumOfSeconds
   , @ChunkStart
   , @ChunkEnd
  )
  
  INSERT INTO DayChunks
  (
   ChunkNumOfMinutes
   , ChunkNumOfSeconds
   , ChunkStart
   , ChunkEnd
   , IsLastChunk
  )
  VALUES
  (
   @ChunksNumOfMinutes
   , @ChunksNumOfSeconds
   , @MaxEnd
   , @ChunksStartTime
   , 1
  )
 END
END
GO





IF EXISTS
(
 SELECT * FROM dbo.sysobjects
 WHERE id = object_id(N'[dbo].[CreateDayChunksBasedOnSettings]')
 AND OBJECTPROPERTY(id, N'IsProcedure') = 1
)
 
DROP PROCEDURE [dbo].[CreateDayChunksBasedOnSettings]
GO

CREATE PROCEDURE [dbo].[CreateDayChunksBasedOnSettings]
AS
BEGIN
 DECLARE @ChunksNumOfMinutes INT
 DECLARE @ChunksNumOfSeconds INT
 DECLARE @ChunksStartTime TIME(0)
 
 IF EXISTS (SELECT TOP 1 ID FROM Settings WHERE Name = 'DayChunksNumOfMinutes')
 BEGIN
  SELECT TOP 1 @ChunksNumOfMinutes = 
   CASE
    WHEN Value IS NULL THEN CAST(0 AS INT)
    ELSE CAST (Value AS INT)
   END
  FROM Settings
  WHERE Name = 'DayChunksNumOfMinutes'
 END
 ELSE
 BEGIN
  SET @ChunksNumOfMinutes = 0
 END

 IF EXISTS (SELECT TOP 1 ID FROM Settings WHERE Name = 'DayChunksNumOfSeconds')
 BEGIN
  SELECT TOP 1 @ChunksNumOfSeconds = 
   CASE
    WHEN Value IS NULL THEN CAST(0 AS INT)
    ELSE CAST (Value AS INT)
   END
  FROM Settings
  WHERE Name = 'DayChunksNumOfSeconds'
 END
 ELSE
 BEGIN
  SET @ChunksNumOfSeconds = 0
 END

 IF EXISTS (SELECT TOP 1 ID FROM Settings WHERE Name = 'DayChunksStartTime')
 BEGIN
  SELECT TOP 1 @ChunksStartTime = 
   CASE
    WHEN Value IS NULL THEN CAST('00:00:00' AS TIME(0))
    ELSE CAST (Value AS TIME(0))
   END
  FROM Settings
  WHERE Name = 'DayChunksStartTime'
 END
 ELSE
 BEGIN
  SET @ChunksStartTime = CAST('00:00:00' AS TIME(0))
 END
 
 EXEC [dbo].[CreateDayChunksBasedOnInput] @ChunksNumOfMinutes, @ChunksNumOfSeconds, @ChunksStartTime
END
GO





SET ANSI_NULLS ON
GO

IF EXISTS
(
 SELECT *
 FROM INFORMATION_SCHEMA.ROUTINES
 WHERE ROUTINE_NAME = 'GetDateTimeDayChunkID'
 AND ROUTINE_SCHEMA = 'dbo'
 AND ROUTINE_TYPE = 'FUNCTION'
)
 
DROP FUNCTION [dbo].[GetDateTimeDayChunkID]
GO

CREATE FUNCTION [dbo].[GetDateTimeDayChunkID] 
(
 @InputDateTime DATETIME
)
RETURNS INT
AS
BEGIN
 DECLARE @Result INT
 DECLARE @InputTime TIME(0) = @InputDateTime
 
 SELECT TOP 1 @Result = ID
 FROM DayChunks
 WHERE @InputTime BETWEEN ChunkStart AND ChunkEnd
 
 IF(@Result IS NULL)
 BEGIN
  SET @Result = (SELECT TOP 1 ID FROM DayChunks WHERE IsLastChunk = 1)
 END
 
 RETURN @Result
END
GO

Testing Solution
The script below tests the solution by simulating sensor readings every one second and the bulk aggregation process is held every one minute.
USE [DayChunks]
GO


-- Setting day chunks settings
-- DayChunksStartTime = '00:00:00'
-- DayChunksNumOfMinutes = 1
-- DayChunksNumOfSeconds = 0
-- This means that the day starts at 00:00:00 and is splitted into chunks each is 1 minute long
EXEC [dbo].[SetDayChunksSettings] 1, 0, '00:00:00'

-- Splitting the day into proper chunks based on the settings set in the previous step
EXEC [dbo].[CreateDayChunksBasedOnSettings]
GO


-- Simulating sensor readings every one second
TRUNCATE TABLE SensorReadings
GO

DECLARE @i INT;
DECLARE @SensorReading FLOAT
DECLARE @Now DATETIME

SET @i = 1;
WHILE (@i <= 90)
BEGIN
 WAITFOR DELAY '00:00:01'
 
 DECLARE @CreationDateTime1 DATETIME = GETDATE()
 DECLARE @RandomReading1 FLOAT
 SELECT TOP 1 @RandomReading1 = RAND()
 
 INSERT INTO SensorReadings
 (
  Reading
  , CreationDateTime
  , DayChunkID
 )
 VALUES
 (
  @RandomReading1
  , @CreationDateTime1
  , [dbo].[GetDateTimeDayChunkID](@CreationDateTime1)
 )
 
 SET  @i = @i + 1;
END


SELECT *
FROM SensorReadings


-- Applying aggregation on sensor readings
TRUNCATE TABLE AggregatedSensorReadings
GO

INSERT INTO AggregatedSensorReadings
(
 DayChunkID
 , Reading
 , DayDate
)
SELECT DayChunkID
, AVG(Reading)
, DayDate
FROM SensorReadings
GROUP BY DayDate, DayChunkID


SELECT AggregatedSensorReadings.*
, DayChunks.ChunkStart
, DayChunks.ChunkEnd
FROM AggregatedSensorReadings
LEFT OUTER JOIN DayChunks
ON DayChunks.ID = AggregatedSensorReadings.DayChunkID

Result

 Settings


DayChunks 1




DayChunks 2


SensorReadings 1


SensorReadings 2


AggregatedSensorReadings



That's it. This is just an example of an application of the main concept of splitting day time into chunks. You can still use this concept and adapt the code to your business needs.


Hope this will help you someday.
Good Luck.