Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

This takes in an array of data built from a source workbook, builds a set of "flags" based on the data in each row of the array. Then it creates a new finalRng array that will, based on a set of logic from the business, create new rows based on the data.

I think there are a ton of ways to accomplish what I am looking for but this code is what I have come up with so far (code is in a UserForm module):

Option Explicit
Dim nonLoanCodes As Variant
Dim sourceColumns As Variant
Dim finalRng()
Dim trimmedRange As Variant
Dim log As New Logger
Dim beforeTaxPercentSum As Double
Dim beforeTaxFlatSum As Long
Dim rothPercentSum As Double
Dim rothFlatSum As Long
Dim logMessage As String
Dim strFirstFile, strSecondFile, strThirdFile As String
Dim wbkFirstFile, wbkSecondFile, wbkThirdFile, wbkConfigFile As Workbook

Private Sub btnBuildImportFile_Click()
'
' This function will build a 401k/Loan Worksheet for upload into UltiPro
'

On Error GoTo ErrorHappened
    Dim lastRow As Long
    Dim allRowFlags() As FlagBag
    Dim payrollDate As String
    Dim cell As Range

    Application.DisplayAlerts = False
    payrollDate = cmbPayrollDate.Value

    'Declare source and destination workbooks
    strFirstFile = lblFileName.Caption
    strSecondFile = ThisWorkbook.path & "\template.xlsx"
    strThirdFile = ThisWorkbook.path & "\ultiImport_" + Format(Now, _
        "yyyy_mm_dd-hh_mm") + ".xlsx"
    Set wbkFirstFile = Workbooks.Open(strFirstFile)
    Set wbkSecondFile = Workbooks.Open(strSecondFile)

    'Function call to validate whether the chosen source file is formatted correctly
    If IsValidImportSheet(wbkFirstFile) = False Then
        lblFileName.Caption = ""
        Label2.Caption = ""
        wbkFirstFile.Close
    Else
        'Function call to get last row from source sheet and build the source range and destination columns
        lastRow = GetLastRowOnSheet(wbkFirstFile)

        'Grab only the necessary values from the source worksheet and put them into an array
        With wbkFirstFile.Sheets(1).Range("H2:W" & lastRow)
            trimmedRange = Application.Index(.Value2, .Worksheet.Evaluate("ROW(" & _
                .Columns(1).Address & ")-1"), Array(1, 9, 10, 14, 15, 16))
        End With

        'loop input file temp range and set flags/properties for each record
        CalculateRowValues allRowFlags()

        Worksheets.Add
        ActiveSheet.Name = "Temp"
        ActiveSheet.Move After:=Sheets(ActiveWorkbook.Sheets.count)
        Range("A1:G" & UBound(finalRng)) = finalRng

        'Write all to output template
        ActiveSheet.Range("A1:A" & UBound(finalRng)).Copy _
            Destination:=wbkSecondFile.Sheets(1).Range("B3:B" & UBound(finalRng))
        ActiveSheet.Range("F1:F" & UBound(finalRng)).Copy _
            Destination:=wbkSecondFile.Sheets(1).Range("C3:C" & UBound(finalRng))
        ActiveSheet.Range("B1:B" & UBound(finalRng)).Copy _
            Destination:=wbkSecondFile.Sheets(1).Range("F3:F" & UBound(finalRng))
        ActiveSheet.Range("C1:C" & UBound(finalRng)).Copy _
            Destination:=wbkSecondFile.Sheets(1).Range("G3:G" & UBound(finalRng))

        'Apply payroll date to column N
        For Each cell In wbkSecondFile.Sheets(1).Range("N3:N" & _
            UBound(finalRng) + 2)
            cell.Value = cmbPayrollDate.Value
        Next cell

        wbkFirstFile.Close
        wbkSecondFile.Sheets("Temp").Delete

        'Save template as a new file
        SaveActiveSheet wbkSecondFile, strThirdFile
        wbkSecondFile.Close

        'Log totals to external file
        logMessage = ("New Entry: " & Chr(13) & Chr(13) & "Source File Name: " & _
            strThirdFile & Chr(13) & Chr(13) & "Total Rows On Source Sheet: " & _
            UBound(trimmedRange) & Chr(13) & Chr(13) & "Before Tax Deduction Percent Sum: " & _
            beforeTaxPercentSum & Chr(13) & Chr(13) & "Before Tax Deduction Flat Sum: " _
            & beforeTaxFlatSum & Chr(13) & Chr(13) & "Roth Percent Sum: " & _
            rothPercentSum & Chr(13) & Chr(13) & "Roth Flat Amount Sum: " & rothFlatSum _
            & Chr(13))

        log.logEntry (logMessage)

        PostBuildResultsAndCleanup strThirdFile

        Application.DisplayAlerts = True
    End If

ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:btnBuildImportFile_Click"
    Resume ExitNow
    Resume
End Sub
Function IsValidImportSheet(ByVal book As Workbook) As Boolean
    Dim c, rLastCell As Range
    Dim columnLetter, importRange As String
    Dim counter As Integer: counter = 1

    Set rLastCell = book.Sheets(1).Cells.Find(What:="*", After:=book.Sheets(1).Cells(1, 1), LookIn:=xlFormulas, LookAt:= _
        xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious, MatchCase:=False)
    columnLetter = Col_Letter(CInt(rLastCell.Column))
    importRange = "A1:" & columnLetter & "1"

    For Each c In book.Worksheets(1).Range(importRange).Cells
        If sourceColumns(counter, 1) <> c.Value Then
            IsValidImportSheet = False
            MsgBox "Column: " & Chr(34) & c.Value & Chr(34) & " not expected.  Import cancelled."
            Exit Function
        End If
        counter = counter + 1
    Next
    IsValidImportSheet = True
 End Function
Function Col_Letter(lngCol As Long) As String
    Dim vArr
    vArr = Split(Cells(1, lngCol).Address(True, False), "$")
    Col_Letter = vArr(0)
End Function
Function CalculateRowValues(ByRef allRowFlags() As FlagBag)
On Error GoTo ErrorHappened
Dim i, cnt As Long
ReDim allRowFlags(UBound(trimmedRange) - 1)
For i = 1 To UBound(trimmedRange, 1)
    Dim rowFlag As FlagBag

    'Case statements will not work here as we have multiple scenarios that need to be checked
    'Handles all rows where there is only one deduction type
    If trimmedRange(i, 2) > 0 Then
        trimmedRange(i, 2) = trimmedRange(i, 2) * 0.01
    End If
    If trimmedRange(i, 4) > 0 Then
        trimmedRange(i, 4) = trimmedRange(i, 4) * 0.01
    End If
    If trimmedRange(i, 6) = "Y" Then
        With rowFlag
            .fiftyPlus = True
            .rowNumber = i + 1
        End With
    End If
    If trimmedRange(i, 2) >= 0 And IsEmpty(trimmedRange(i, 2)) = False Then
        'Sets 401CP
         If rowFlag.fiftyPlus = True Then
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(1, cnt) = trimmedRange(i, 2)
            finalRng(5, cnt) = nonLoanCodes(3, 1)
            beforeTaxPercentSum = beforeTaxPercentSum + trimmedRange(i, 2)
            cnt = cnt + 1
        'Sets 401P
        Else
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(1, cnt) = trimmedRange(i, 2)
            finalRng(5, cnt) = nonLoanCodes(1, 1)
            beforeTaxPercentSum = beforeTaxPercentSum + trimmedRange(i, 2)
            cnt = cnt + 1
        End If
    End If
    If trimmedRange(i, 3) >= 0 And IsEmpty(trimmedRange(i, 3)) = False Then
        'Sets 401CF
        If rowFlag.fiftyPlus = True Then
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(2, cnt) = trimmedRange(i, 3)
            finalRng(5, cnt) = nonLoanCodes(4, 1)
            beforeTaxFlatSum = beforeTaxFlatSum + trimmedRange(i, 3)
            cnt = cnt + 1
         'Sets 401F
        Else
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(2, cnt) = trimmedRange(i, 3)
            finalRng(5, cnt) = nonLoanCodes(2, 1)
            beforeTaxFlatSum = beforeTaxFlatSum + trimmedRange(i, 3)
            cnt = cnt + 1
        End If
    End If
    If trimmedRange(i, 4) >= 0 And IsEmpty(trimmedRange(i, 4)) = False Then
        'Sets ROTHC
        If rowFlag.fiftyPlus = True Then
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(1, cnt) = trimmedRange(i, 4)
            finalRng(5, cnt) = nonLoanCodes(7, 1)
            rothPercentSum = rothPercentSum + trimmedRange(i, 4)
            cnt = cnt + 1
         'Sets ROTH
        Else
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(1, cnt) = trimmedRange(i, 4)
            finalRng(5, cnt) = nonLoanCodes(5, 1)
            rothPercentSum = rothPercentSum + trimmedRange(i, 4)
            cnt = cnt + 1
        End If
    End If
    If trimmedRange(i, 5) >= 0 And IsEmpty(trimmedRange(i, 5)) = False Then
        'Sets ROTHFC
        If rowFlag.fiftyPlus = True Then
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(2, cnt) = trimmedRange(i, 5)
            finalRng(5, cnt) = nonLoanCodes(8, 1)
            rothFlatSum = rothFlatSum + trimmedRange(i, 5)
            cnt = cnt + 1
         'Sets ROTHF
        Else
            ReDim Preserve finalRng(5, cnt)
            finalRng(0, cnt) = trimmedRange(i, 1)
            finalRng(2, cnt) = trimmedRange(i, 5)
            finalRng(5, cnt) = nonLoanCodes(6, 1)
            rothFlatSum = rothFlatSum + trimmedRange(i, 5)
            cnt = cnt + 1
        End If
    End If

    allRowFlags(i - 1) = rowFlag
    rowFlag.deductionCode = ""
    rowFlag.fiftyPlus = False
    rowFlag.rowNumber = 0
Next i
finalRng = Application.Transpose(finalRng)
ExitNow:
On Error Resume Next

Exit Function
ErrorHappened:
MsgBox Err.Description, vbCritical, "Main:CalculateRowValues"
Resume ExitNow
Resume
End Function
Function GetLastRowOnSheet(ByVal book As Workbook) As Long
'
'This function will get the last used row on the source spreadsheets
'
On Error GoTo ErrorHappened

    Dim TempRange As Range
    Set TempRange = book.Sheets(1).Cells.SpecialCells(xlCellTypeLastCell)
    GetLastRowOnSheet = TempRange.Row

ExitNow:
On Error Resume Next

Exit Function
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:GetLastRowOnSheet"
    Resume ExitNow
    Resume
End Function
Sub SaveActiveSheet(ByVal book As Workbook, ByVal fileName As String)
'
'Saves the active sheet to a new workbook excluding the code tabs
'
On Error GoTo ErrorHappened
    Dim ws As Worksheet

    For Each ws In book.Worksheets 'SetVersions
        If ws.Name = "Upload Template" Then
            Dim wb As Workbook
            Set wb = ws.Application.Workbooks.Add
            ws.Copy Before:=wb.Sheets(1)
            wb.SaveAs fileName
            Set wb = Nothing
        End If
    Next ws

ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:SaveActiveSheet"
    Resume ExitNow
    Resume
End Sub
Sub CleanNewBook()
'
'Cleans the new workbook by removing superfluous tabs
'
On Error GoTo ErrorHappened

    Dim ws As Worksheet
    For Each ws In wbkThirdFile.Worksheets
        If ws.Name <> "Upload Template" Then
            ws.Delete
        End If
    Next ws
    wbkThirdFile.Sheets(1).Name = "Sheet1"
    wbkThirdFile.Save
ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:CleanNewBook"
    Resume ExitNow
    Resume
End Sub
Sub PostBuildResultsAndCleanup(ByVal resultFileName As String)
'Cleans up form and files as well as displays build results
On Error GoTo ErrorHappened
    Set wbkThirdFile = Workbooks.Open(resultFileName)
    BtnBuildImportFile.Enabled = False
    CleanNewBook
    wbkThirdFile.Close
    lblFileName.Caption = ""
    Label2.Visible = False
    txtDetails.Text = Replace(logMessage, "New Entry: " & Chr(13), "")
    Label3.Visible = True
    lblResultFile.Caption = resultFileName
    lblResultFile.MousePointer = fmMousePointerUpArrow
    MsgBox "File Saved: " & resultFileName

ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:PostBuildResultsAndCleanup"
    Resume ExitNow
    Resume
End Sub
Private Sub BtnChooseSourceFile_Click()
'
' Display Windows OpenFileDialog for choosing the input file
'
On Error GoTo ErrorHappened
    Dim oFilePicker As New FilePicker

    oFilePicker.SetupFilePicker

ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:btnChooseSourceFile_Click"
    Resume ExitNow
    Resume
End Sub
Private Sub LblResultFile_Click()
'
'Sets up hyperlink for resulting file
'
On Error GoTo ErrorHappened

    Dim pth As String

    If lblResultFile.Caption <> "" Then
        pth = GetDirectory(lblResultFile.Caption)
        'link = pth
        Unload Main
        ActiveWorkbook.FollowHyperlink Address:=pth, NewWindow:=True

    Else
        MsgBox "Sorry, No Link Available"
    End If

ExitNow:
On Error Resume Next

Exit Sub
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:lblResultFile_Click"
    Resume ExitNow
    Resume
End Sub
Function GetDirectory(fileName)
'
'Strips filename from the path to support the hyperlink for the result file
'
On Error GoTo ErrorHappened

   GetDirectory = Left(fileName, InStrRev(fileName, "\"))

ExitNow:
On Error Resume Next

Exit Function
ErrorHappened:
    MsgBox Err.Description, vbCritical, "Main:GetDirectory"
    Resume ExitNow
    Resume
End Function

Private Sub UserForm_Click()

End Sub

Private Sub UserForm_Initialize()
    Dim payrollDateListItems As Variant
    Dim strConfigFile As String


    strConfigFile = ThisWorkbook.path & "\configuration.xlsx"
    Set wbkConfigFile = Workbooks.Open(strConfigFile)
    nonLoanCodes = wbkConfigFile.Sheets(1).Range("A2:A9")
    payrollDateListItems = wbkConfigFile.Sheets(3).Range("A2:A27")
    sourceColumns = wbkConfigFile.Sheets(4).Range("A2:A44")
    Main.cmbPayrollDate.List = payrollDateListItems
    wbkConfigFile.Close
End Sub
share|improve this question
    
As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does, not what you want to get out of a review. – Jamal Jan 13 at 18:08
1  
Fun fact: your fiddle code parses as-is in Rubberduck v1.4.3; code inspections highlight 40-some issues (including some dead code), and the extract method refactoring could be helpful - I'll see what Rubberduck 2.0 has to say tonight (I maintain the Rubberduck project), and post an answer. – Mat's Mug Jan 13 at 18:31
up vote 3 down vote accepted

It's not clear what FlagBag might be. If it's a class, then the variable rowFlag is used but never assigned, which could be a bug. If it's a user-defined type, ...consider making it a class, since you're passing it around as parameters.

I promised I'd run Rubberduck 2.0 code inspections on your code, so here it goes:

Rubberduck 2.0 code inspections window]

After ignoring a few false positives, about 60 issues remained.

Code quality issues

  • Implicit ByRef parameter: parameters in VBA are passed ByRef by default. In many languages (including VB.NET), parameters are passed ByVal by default, which can be confusing; consider being explicit about how you're passing VBA parameters. This applies to lngCol in the Col_Letter function, and to the fileName parameter of the GetDirectory function.

  • Return type is implicitly 'Variant': functions return a value, and it's very seldom that this value needs to be an actual Variant - and when it does, it's still best to be explicit about the function's return type (As Variant). This applies to functions GetDirectory (which should be returning a String) and CalculateRowValues (which... see next point).

  • Non-returning function or property getter: this is either a confusing API, or a bug: functions should return a value - if the return value isn't assigned, the function isn't returning anything. This applies to the CalculateRowValues function, which looks like it should be a Sub, not a Function.

  • Parameter can be passed by value: a parameter that's passed ByRef but that isn't assigned a new value, has very little reasons to not be passed ByVal. This is purely semantics, but it can avoid introducing bugs when maintaining/refactoring the code.

  • Parameter is not referred to: nevermind that one, it pointed to the Logger.LogEntry stub method that I introduced to get the code to compile.

  • Variable is not referred to: variable payrollDate is never used in the Click handler for btnBuildImportFile. It's never referred to, but you're still assigning to it - the assignment is basically a no-op, and the variable could safely be removed.

  • Unassigned variable: the rowFlag variable in CalculateRowValues is never assigned. If FlagBag is a class (like I made it here... that's why I'm getting this inspection result), you have a runtime error 91 there. If it's a UDT, all is good.

  • Variable is used but not assigned: the allRowFlags() array is passed to the CalculateRowValues function/procedure from the BtnBuildImportFile_Click handler, but it's not initialized or used afterwards - perhaps it should be local to CalculateRowValues?

Language opportunities

  • Empty string literal: consider preferring vbNullString over "". It better communicates the intent of the code, and being a null string pointer it doesn't use any memory (vs. "" which takes up... 2 whole entire bytes).

  • Variable is implicitly 'Variant': a variable whose type isn't specified is implicitly declared as Variant, which isn't typically what you intend to do. For example, only the last variable is of the specified type here, strFirstFile, strSecondFile, wbkFirstFile and wbkSecondFile are all Variant:

    Dim strFirstFile, strSecondFile, strThirdFile As String
    Dim wbkFirstFile, wbkSecondFile, wbkThirdFile, wbkConfigFile As Workbook
    

    this is a common beginner mistake, which leads to...

Maintainability & readability issues

  • Instruction contains multiple declarations: avoid declaring more than 1 variable on a single instruction/line. Your code has 5 instances of this issue, and every time, you're declaring implicit Variant variables without realizing.

  • Implicitly public member: module members in VBA are Public by default. Consider specifying an explicit access modifier, and making members only as visible as they need to be.

  • Use meaningful names: avoid 1-3 character identifiers and disemvoweling, and prefer names that can be pronounced. What does pth mean? Also, consider renaming Label2 and Label3 (and avoid numeric suffixes altogether) so that it's clear what their purpose is. There are exceptions, of course: i is commonly used with a For loop; ws and wb are commonly used for Worksheet and Workbook objects.


+ Other things that Rubberduck didn't pick up:

If rowFlag.fiftyPlus = True Then

Can be written as:

If rowFlag.fiftyPlus Then

Similarly:

And IsEmpty(trimmedRange(i, 2)) = False Then

Can be written as:

And Not IsEmpty(trimmedRange(i, 2)) Then

Avoid comparing Boolean values to Boolean literals in Boolean expressions, it's... redundant ;-)


Looking at the individual code blocks, it looks very much like a lot of copy/paste is going on:

        ReDim Preserve finalRng(5, cnt)
        finalRng(0, cnt) = trimmedRange(i, 1)
        finalRng(2, cnt) = trimmedRange(i, 5)
        finalRng(5, cnt) = nonLoanCodes(8, 1)
        rothFlatSum = rothFlatSum + trimmedRange(i, 5)
        cnt = cnt + 1

This body is repeated in every block, with slightly different values. Consider extracting a method - Rubberduck gives you the perfect tool to do this:

Rubberduck's Extract Method refactoring tool

It knows that finalRng, trimmedRange and nonLoanCodes arrays are module-scope, as well as beforeTaxPercentSum - there's apparently a little glitch here, since the i parameter can be passed ByVal (but then, there's a code inspection to fix that), but the cnt variable is correctly passed ByRef (because it's assigned a new value that the caller needs to see).

All you need to do once that method is extracted, is to add another parameter for what needs to change between calls:

Private Sub ExtractedMethod(ByRef cnt As Long, ByVal i As Variant, ByVal nonLoanCodeIndex As Long, ByVal trimmedRangeIndex As Long)
    ReDim Preserve finalRng(5, cnt)
    finalRng(0, cnt) = trimmedRange(i, 1)
    finalRng(1, cnt) = trimmedRange(i, 2)
    finalRng(5, cnt) = nonLoanCodes(nonLoanCodeIndex, 1)
    beforeTaxPercentSum = beforeTaxPercentSum + trimmedRange(i, trimmedRangeIndex)
    cnt = cnt + 1
End Sub

And then you can replace a bunch of redundant code blocks with calls to this new extracted method:

If trimmedRange(i, 2) >= 0 And Not IsEmpty(trimmedRange(i, 2)) Then
    If rowFlag.fiftyPlus = True Then
        ExtractedMethod cnt, i, 3, 2
    Else
        ExtractedMethod cnt, i, 1, 2
    End If
End If
If trimmedRange(i, 3) >= 0 And Not IsEmpty(trimmedRange(i, 3)) Then
    If rowFlag.fiftyPlus = True Then
        ExtractedMethod cnt, i, 4, 3
    Else
        ExtractedMethod cnt, i, 2, 3
    End If
End If

Do that again for the rothPercentSum and rothFlatSum blocks, and then refactor / extract method again to move the redundant code in one place, since there's still repetition between the extracted blocks.

Remember to use meaningful names - and if you come up with a better name after you've referenced an extracted method in 20 places, it's never to late to rename it:

Rubberduck's Rename refactoring

A "rename" refactoring is different (and safer!) vs. a search & replace, since it searches resolved identifier references rather than text in a code module.

Refactoring code can be done manually. But using refactoring tools can make tedious and risky changes almost instant, ...and almost fun!


Fields strFirstFile, strSecondFile and strThirdFile can be local to the btnBuildImportFile_Click handler, since they're only ever used in that scope. Same with wbkConfigFile, which can be local to UserForm_Initialize.


share|improve this answer
    
Thank you so much for this. I spent some time refactoring yesterday to remove the duplicated code blocks to a function like you suggested above and I removed the flagBag type as well. It was an artifact from an older version of the code. I will look at all the suggestions and make some changes today. So I assume if I affectively make these changes my code should be pretty solid, right? – VinnyGuitara Jan 15 at 14:01
2  
What is rubberduck? Is it a tool anyone can use? – VinnyGuitara Jan 15 at 14:02
1  
@VinnyGuitara version 2.0 is still under development, but you can get the latest release (v1.4.3) on rubberduck-vba.com - the project is open-source and originated right here on CR; see the rubberduck tag wiki and our GitHub repository for more information. – Mat's Mug Jan 15 at 14:33
1  
Tremendous, thanks so much for this information. – VinnyGuitara Jan 15 at 14:51
    
@VinnyGuitara the truth is, addressing all issues pointed out by Rubberduck will only make the code less ambiguous and easier to read - refactoring to eliminate redundancies and adhering to the Single Responsibility Principle is the key to making your code more flexible and easier to maintain. One important point I didn't mention, is that you seem to have the whole logic coded behind a UserForm, which isn't ideal - consider moving the logic to dedicated class modules, closer to an actual OOP paradigm. – Mat's Mug Jan 15 at 15:05

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.