TweetFollow Us on Twitter

Associative Arrays

Volume Number: 16 (2000)
Issue Number: 10
Column Tag: Programming Techniques

How to Harness the Power of Associative Arrays

by Kas Thomas

A widely underutilized technique can go a long way toward making your code smaller, more reliable, and easier to maintain, whether you program in C, Java, or JavaScript

Fostering reliability, maintainability, compactness, and good performance in code has been a constant quest for programmers and language designers over the years. It's rare that one technique can give you all of the above benefits, concurrently. But intelligent use of associative arrays can do that. If you haven't yet tapped into the power of associative arrays, you might want to give the issues involved some thought. The issues are widely applicable to a variety of programming tasks, cutting across all major languages.

One thing's for sure. If you use a lot of switch statements in your code (to "case out" user-selected actions, for example), you're sitting on a great opportunity to improve the reliability and maintainability of your code; just follow along and see.

What Is an Associative Array?

An associative array is a collection of data values in which access to any particular value is obtained via a (single) key. Generally speaking, it's a one-to-one mapping of data entities ("values") to keys, such that no two values map to the same key (although two different keys could, coincidentally, contain the same value). For example, in an array of TV listings, you might very well find that two different channels are showing the same episode of Gilligan's Island at 4:00 a.m. Tuesday, but you would never find Channel 2 listed twice, showing two different shows in the same time slot.

In a larger sense, associative-array mapping is an application of set theory, and languages that have a robust collections package will invariably implement some form of associative-array mapping.

For a quick example of an associative array, consider the hypothetical entity in Figure 1.


Figure 1. A hypothetical associative array.

In this example, we have an anonymous (nameless) array of tuples shown as keys mapped to values. The itemType key, for example, maps to the value 'auto'. If this were a conventional (ordered) array, we would say that the zeroth item in the array is the (string) value 'auto', the first item is '4WD', etc.

For the array shown in Figure 1 to be useful, we need to be able to hold a reference to the array in a variable, and we need to have a syntax for obtaining the value of each member, given the value of the corresponding key. It would also be nice if we could find out how many items are in the array (since it might be arbitrarily large) and to know how to iterate through the members. With an ordered array, you can step through the members in sequence. But in an array like this one, where the keys aren't integers, how do you enumerate the array's contents?

Associative Arrays in JavaScript

Core JavaScript (or ECMAScript) has a convenient syntax for dealing with constructions such as this. (Even if you're not a JavaScript programmer, bear with me here, because I'll be discussing other languages in a moment.) In the world of object-oriented programming, an arbitrary collection of data entities is often called an object. In JavaScript, a generic object is, in fact, an associative array (whose members can be references to functions, in which case those particular members are called methods). In the OOP world, dot notation is commonly used to "index into" an object. For example:

var myObject =   new Object();
myObject.itemType =   'auto';
myObject.subtype =   '4WD';
myObject.make =         'Jeep';
myObject.year =         '1998';
myObject.color =      'green';

In JavaScript, this is an entirely acceptable way to implement the object suggested by Figure 1. An equivalent syntax that uses literal notation is:

var myObject = { 'itemType' :   'auto',
                        'subtype'  :   '4WD',
                         'make'     :   'Jeep',
                         'year'     :   '1998',
                        'color'    :   'green' };

But are we justified in calling this an associative array? Yes, we are, because it turns out we can also use array notation (square brackets) in JavaScript to refer to object properties:

var myObject =   new Object();
myObject['itemType'] =      'auto';
myObject['subtype'] =      '4WD';
myObject['make'] =         'Jeep';
myObject['year'] =         '1998';
myObject['color'] =         'green';

if (myObject['itemType'] ==   'auto' 
  && myObject['make'] ==         'Jeep')       // true
      myObject['subtype'] =      '4WD';   // assign '4WD' to myObject.subtype

In JavaScript, square brackets can substitute for dot notation when referring to object properties. The only proviso is, the enclosed property name must be supplied as a string literal (as shown above). This turns out to be an extremely handy syntactical construct, because it means we are not limited to one-word property names. We can now consider constructions like:

myObject['extended warranty'] = true; // legal myObject['original delivery date'] = '1/1/98'; // legal myObject.'original delivery date' = '1/1/98'; // ERROR!

Iterating through the contents of an associative array (or the properties of an object) would seem to present a problem, since the contents aren't necessarily ordered and can't be "indexed into" numerically. In JavaScript, however, there is a very handy loop syntax for doing this:

var bigstring = new String();

for (var i in myObject)
    bigstring += i + "\n";

The above code builds a list of all the property names in myObject (separated by newlines), concatenating them into a single big string.

Perl

Perl has a built-in associative-array construct called the hash. The syntax for creating a hash looks like:

%myObject = ('itemType' , 'auto', 'subtype' , '4WD', 'make' , 'Jeep', 'year' , '1998', 'color' , 'green');

Notice that commas are used throughout the pairs list, rather than the more readable JavaScript technique of putting colons between keys and values (using commas to separate complete tuples).

To access a hash value in Perl, you use a notation that looks like:

$myObject{ 'color' } = 'red';

Notice the seemingly inconsistent use of the dollar sign (instead of the percent sign) and curly braces (instead of parentheses) for accessing individual values of a hash. This is a standard Perl idiom. The percent sign refers only to complete hashes.

Java

With the advent of the Java 2 platform , the java.util package now has a powerful Collections Framework (not to be confused with JGL, the Generic Collections Library for Java by ObjectSpace, which was first-to-market and was widely distributed in IDEs before Sun came out with the J2SE Collections Framework), encompassing sorted and unsorted sets, maps, and lists, plus iterators for same, in thread-safe and non-thread-safe versions. Prior to Java 2, the language had Vector and HashTable classes to accommodate this need, but those classes have been relegated to legacy status. Today, if you need an associative array, you call on the HashMap class, which implements the Map interface:

Map map = new HashMap();
map.put("itemType", "auto");
map.put("subtype" , "4WD");
map.put("make"    , "Jeep");
map.put("year"    , "1998");
map.put("color"   , "green");

For sorted maps, you can create a TreeMap (using a Map as input, if necessary), which stores its elements in an always-balanced tree.

C and C++

In C++, the Standard Template Library provides rich support for collections idioms of all kinds. The map is an associative container that accomplishes the kind of functionality we've been talking about:

map<string,string> myMap;
  
myMap["itemType"] = "auto";
months["subtype"] = "4WD";

// etc. etc.

To use it, be sure to #include <map>. See any good reference on STL for further details.

ANSI C, on the other hand, has no built-in support for associative arrays. You can craft your own support, of course, with a little effort. But how? As you might guess from the repeated use of the strange term "hash" in this context, implementing an associative array from scratch requires the use of hash techniques.

What, exactly, is a hash? A hash is basically a mathematical function in the canonical sense: a "black box" that maps domain values to range values, with the requirement that any one domain value can map to no more than one range value. This is exactly the mapping behavior we're looking for, of course.

But what does a hash function really look like? The answer is, it can look like whatever you want it to look like, or (more commonly) whatever you need it to look like. If that sounds a bit vague, that's because hash techniques fall into a curious category of computational heuristics with a long background in computer-science lore. In the old days of limited machine resources, you'd use hash techniques to maximize performance while minimizing storage requirements. Good hash functions weren't often published, and if they were, their workings were rarely self-evident.

A Hash-Code Example

A quick example may help. Suppose you are implementing an exception dictionary for a spellchecker: i.e., a special dictionary to store user-entered words. What you want is that when a user types a word that is not in the regular dictionary, the exception dictionary can be consulted; and at that point, you simply need to know whether the word exists in that dictionary. (If not, you flag the word as a possible misspelling and notify the user.) You have a maximum of 5 Kbytes of storage available for this task, because you're running on a 1970s machine with severely limited resources.

A typical answer to this challenge would be to insert exception words into a linked list, and do a lookup by traversing the list. With 5K of storage and an average word length of five characters, you could store as many as 1,000 words in the list (more than adequate for most exception dictionaries). But lookups will be time-proportional to the number of words in the list, and there will be significant overhead in terms of list management, because each time a new word is entered in the list, you have to be sure it doesn't already exist.

This approach gets a D for ingenuity.

A bright young programmer in your department comes to you at this point and says "Wait! I have a better idea." He explains that if you keep the linked list sorted alphabetically, you can speed lookups because they will occur in time-proportion to the log of the number of entries. List-maintenance overhead is reduced as well, although storage requirements haven't changed.

This represents a significant improvement. Let's give it a C+ (or maybe even a C++) for effort.

At this point, a consultant walks in the door and offers to solve the problem for you in such a way that up to 40,000 words can be stored in your exception dictionary and lookups are instantaneous, with zero table maintenance. You say to him "Thank you, but that's clearly impossible. Don't let the doorknob hit you in the butt on the way out." He goes straight to your competitor, implements the solution, and eventually the competitor puts you out of business. You end up face-down in the gutter, next to an empty bottle of Ripple.

How did the consultant do it? First of all, since you are only concerned with whether a given word has an entry (true) in the dictionary or not (false), you're really looking for a binary (Boolean) answer to a question; hence your 5K dictionary can really store 40K lookups (assuming 8-bit bytes, of course). The dictionary needs to be implemented as a bit table.

To implement a direct lookup of words, we resort to the following heuristic. To store a word in the dictionary, we submit the word string to a hash function, which in turn converts the word from a string to a number in the range of zero to 5K-minus-one. We then index into the bit table at the offset thus obtained, and store the word by flipping the bit "on." (A zero bit means the word doesn't exist in the table.) To look up a word and see if it exists in the exception dictionary, we simple hash the word and do a direct lookup. If the entry in the table at that lookup is true, the word exists. If not, it doesn't. Problem solved.

Finally, an A+ solution!

Hash Functions

But we still haven't resolved the question of what a hash function looks like. Earlier we merely said that it could look like whatever it needs to look like. Let's take the exception-dictionary example we just discussed. How many bits' worth of information does a 5-character string really hold? If the string represents an English word, each character can have one of 26 possible values (except that the first character can be upper or lower case; hence 52 possible values). Storing 26 possibilities requires 4.7 bits; 52 possibilities requires 5.7 bits. The average word therefore requires 5.7 + 4 * 4.7 == just under 25 bits of bandwidth. Every 5-character English word can therefore be converted (mapped directly and unambiguously) to a 25-bit number. Which is to say, a number between zero and 33,554,431.

Clearly, if we had 32 million bits (4 megabytes) of storage available, and words could never be longer than five characters, you could convert words directly to binary numbers and use the numbers as offsets into a bit table.

Unfortunately, in the real world, words are not constrained to five characters in length and four megabytes of table storage are not always available. In fact, our example calls for no more than 5 Kbytes (40Kbits) of storage.

Why not take the bottom 15 bits of each word (discarding the rest) and use that as an index into a 40Kbit table? The answer should be obvious. Words often have the same endings. Taking the last 15 bits of 'eating' and 'drinking' would result in both words contending for the same spot in the exception dictionary. Totally unsatisfactory.

The crux of understanding how a hash solution works in this kind of situation is to think long and carefully about the problem. If you think about it, you will realize that the ASCII values in words are not at all randomly chosen. The composition of word strings in English is constrained by spelling rules, which in turn are determined, in part, by constraints on the speakability of syllables (i.e., the geometry of the human voice apparatus). The average English word may consume only 25 bits of bandwidth, but there most certainly are not 32 million different 5-letter words in the English language! The actual number of different 5-letter words you might encounter might only be in the low thousands. Certainly, in a spellchecker context, 5-letter "exception" words (words that are not commonly found in English) would likely be numbered in the low hundreds, if that many.

Careful analysis of the problem space should convince you that what we're really dealing with here is a sparse data set. We will be mapping a few hundred exceptions (or at most, a few thousand) into a 40,000-bit table. All we need is a way to convert words to numbers that map well across the available domain.

Suppose we design a hash function in C as shown in Listing 1 (where ch is a pointer to the word string).

Listing 1: hash()

hash()

unsigned int hash(ch) {

   unsigned int hashvalue = 0;

    if (!*ch) return 0; // sanity

   do {
      hashvalue += *ch++;
      hashvalue *= 13; 
   } while (*ch);

  return (hashvalue % TABLESIZE);
}

This hash function adds the ASCII value of each character in the string, one by one, to a temporary variable. But in between each addition, the variable is multiplied by the magic number 13, which (in binary terms) is equivalent to a left-shift by log(13) bits. (Prove to yourself that this is so.) What's the significance of 13? Nothing. It simply works. Might some other number work better? Yes! That's the magic of hash functions. Often, you can "tune" them to make them work better.

What does "work better" mean? It means you have fewer situations where two different character strings hash to the same numeric value. When two data objects hash to the same value, it's called a hash collision. Collisions are usually undesirable. In our exception-dictionary example, it could mean that a nonsense word maps to the same dictionary location as a perfectly valid word!

Dealing with Collisions

There are various ways of handling collisions. The most common way (not for bit tables, but for tables where actual string information might be stored) is to fork a linked-list node at the point of the collision. Another method is to keep parallel tables, but with different hash functions for each. An "entry" in the dictionary would actually consist of an entry in each table. A lookup would verify that Hash 1 produces a positive result from Table 1 and Hash 2 yields a positive result from Table 2. Prove to yourself that (given suitably designed hash functions) if a collision happens in the first hash table, it's extremely unlikely that a collision also occurs, simultaneously, in the other hash table; therefore, the overall collision rate is the product of two small numbers (the individual failure rates of the hashes).

In some situations, collisions can safely be ignored; the "collider" overwrites the old table entry, and that's that. This can actually be desirable in instances where data entities are expected to regularly go out of scope. (In this case, you have what is commonly known as a "weak" associative array). In the spellchecker example, it's debatable whether collisions are important. No spellchecker is ever perfect. If the odds of a misspelled word hashing to the same spot as a legitimate word are less than, say, one in five thousand, would the user really notice? Would he care?

Hash Tuning

The fascinating thing about hash functions like the one above is that they can be tuned for better collision performance (lower collision rates). What makes this fascinating is the fact that, because the problem is so poorly defined (mathematically), an analytical solution is usually out of the question. Therefore, tuning must be done empirically: try a tweak, measure the result. Try another tweak; measure the result. Repeat until happy.

You might want to write a short program that takes a text stream (a document) as input and maps the "vocabulary" to a hash table, measuring collision rates in the process. Find out how collision rate varies with load factor (table consumption) for a given hash function. Then try changing the function in some way, and see how the collision performance changes.

The example hash function given in Listing 1 is actually a surprisingly good hash function for most purposes. With a driver program (such as HashMule; see further below), you can prove to yourself, empirically, that this modest-looking function works much better (i.e., produces many fewer collisions) than the same function with the number 12 substituted for the number 13. (This is why I called 13 a magic number.) In fact, when I ran a quick test of Listing 1 using the Declaration of Independence as the source text (hashed into a table with room for 1024 words), I found that changing the magic number 13 to 12 caused collisions to more than double.

In general, even numbers produce poor results, and non-prime odd numbers (while better) are not as good as primes.

When writing a hash function, you will always need to constrain the final output to a certain range (in our example, 5K). Be aware that, for tuning purposes, it can matter a great deal what the "modulo" constraint is. Again, it turns out that a prime number here will improve performance. So make your lookup table an odd size! (But don't take my word for it. Write a test program and prove it to yourself.)

A Common Hash Misconception

A common misconception is that hash functions are designed to distribute values randomly over a given range. You should convince yourself that this is not true. After all, if hashing were this easy, every hash function would simply return a random number (converted to integer form and scaled to fit the desired range). The ideal hash function is usually one that distributes keys evenly across a range, with no collisions. Random numbers tend to cause collisions, at a rate equivalent to the hashtable load factor. (That is, when the lookup table gets half full, each new entry into the table has a 50:50 chance of generating a collision.) A good hash function can do much better, because hash functions can be constructed to remap data non-randomly. And in fact, this is exactly what you are trying to achieve. You are trying to map a sparse, non-random data set to non-random locations in a table: locations that spread out evenly and don't overlap. If you know a thing or two about the data before you begin, you can make intelligent decisions about how to craft the hash function.

Consider a list of 500 arbitrarily chosen unique words selected from Webster's dictionary at random. Imagine that you are trying to map these words into a hash table that has enough space for all 500 words, but only enough space. (But you don't necessarily know in advance how many words there are.) Can it be done? Of course. You just need to develop a hash function that maps table positions to the words' alphabetical ranking. If you can analyze the word list in advance, this is a piece of cake. If you don't have advance access to the words, but your hash object is free to reorder table data on the fly, again it's not a hard problem, because you can insert words in alphabetical order and reorder as necessary. Even if you know nothing about the words (except that they are words!), you can easily come up with a hash function that does a better job of mapping them to a table (i.e., with fewer collisions) than mere random dart-throwing.

There's no magic procedure for constructing a good hash function. Each situation is different. What might be a great function for one set of conditions could very well be terrible under another set of conditions. The trick is to understand your data set (and what's unique about it) and understand what it is you are trying to do: map members of the set onto intervals that don't overlap.

Putting Associative Arrays to Work

With a hash function approach in C, we haven't quite achieved the table['string'] lookup ease of other languages, but we've approached it quite closely, because we can instead do:

lookup = table[ hash('myItem') ];

But the greater question, at this point is: How can you put associative arrays to work? One example is well-known: In compilers, symbol tables are usually accessed via a hash function that computes a likely place to begin a serial search. Since symbol-table lookup is a very frequent occurrence in compilers, the performance of the hash function (both in raw execution speed and in collision rate) can have a dramatic effect on overall compiler performance.

But what if you're not a compiler writer? In that case, you might want to consider a possible application that applies broadly across a variety of programming tasks in a variety of languages. It involves 'case' statements.

The Case Against Case Statements

Most high-level languages support a switch construct that allows options to be associated with desired actions. In C:

switch( option ) {

   case OPTION_A : do_something();
                         break;

   case OPTION_B :
   case OPTION_C : do_something_else();
                         break;

   // . . . more options processing

   default : do_whatever();
}

Usually, the cases must refer to hard-coded values. Fall-through occurs by default, which means that if you leave a break statement out (such as with OPTION_B above), execution continues with the next case. The original intent of the switch construct was to keep programmers from having to resort to long chains of if/else actions (while leaving code readable). But hindsight has shown switch/case syntax to be a notably poor syntax design in a number of respects. First of all, it doesn't save any typing (over if/else actions). In fact, switch blocks are usually quite long, because in instances where there are only a couple of cases, programmers tend to collapse everything to a few if/elses. The opportunity for typos is high in long code blocks. Also, the default fall-through behavior of switches is known to be a bad design decision, because it's not the default behavior most programmers are looking for, most of the time. (Some years ago, Sun analyzed its C compiler source code to see how often the default fall-through path was actually used. It turned out the Sun ANSI C compiler's front end had 244 switch statements, averaging seven cases each. Fall-through occurred in only 3% of cases.)

There is also a performance issue, in that the compiler must (because of the possibility of fall-through) evaluate each case in turn, up to the first match. But there's an even bigger problem, architecturally: Switch statements require you to intermix code and static data. This leads to poor maintainability and a host of other ills. It would be better, obviously, to segregate code from static data so that when the data needs to be revised periodically, it can be edited without changing any code.

The answer should be obvious by now: Convert switch blocks to associative arrays. Each case can be used, after all, as a key to find the associated action. (If the associated action is a block of code, then the array need only store a pointer to the appropriate function or macro.) In this way, you can collapse the entire switch block down to one line of code, which executes instantly.

Consider the common situation, in CGI/forms programming, of retrieving a user selection from a drop-down menu and taking action on it. Usually, you have something like this:

var userSelection = element.options[i].value;

switch(userSelection) {

   case 'Kansas' :  // JavaScript allows string cases
            do_this();
          break;

    case 'Ohio' : 
          do_that();
          break;

// etc.

   default : whatever();
            break;
}

With an associative array, you can separate data from code and reduce all actions to a single expression. In JavaScript:

// build a lookup table as an Object:
var lookupTable = { 
       'Kansas' : do_this,
       'Ohio'   : do_that,
                 // etc.
       }

// execute action based on lookup:
lookupTable[ userSelection ]( );  

An entire series of if/else statements (or a big, long switch statement) is thus collapsed to one expression, which does one lookup, then vectors to an action procedure. Code is segregated from data, making maintenance easier and less likely to introduce bugs. Performance is improved, many lines of code are reduced to one, and readability doesn't suffer a bit. The best of all worlds. (Just don't tell your competition.)

Sample App: HashMule

For this article, I wrote an educational utility for creating, studying, and comparing the collision rates of hash functions. The app, called HashMule, is actually a PDF form incorporating about 150 lines of JavaScript. It is designed to run under Acrobat Reader version 4.0 or 4.05. You can download HashMule from http://www.acroforms.com/archive/HashMule.pdf, or by FTP from the Mactech website.

One of the things HashMule allows you to do is enter hash functions, then execute them against text in real time. A "Hash It" button on the form will hash every word of a given text block into a hash table, then report statistics such as table load factor and hash collision rate. By making small changes in the hash function and hitting the "Hash It" button, you can see how the collision rate changes as you change "magic number" values or hash-function logic. The text of the Declaration of Independence is included in the form as a sample text block.

For Further Information

Associative arrays are such a staple of the Perl language (where they are called hashes) that you'll find them discussed in detail in every decent Perl book and online reference.

Few JavaScript references spend adequate time discussing that language's built-in associative array capabilities. David Flanagan's JavaScript: The Definitive Guide (O'Reilly) is one book that does.

An outstanding introduction to the Java 2 platform's Collections Framework can be found at http://developer.java.sun.com/developer/onlineTraining/collections/Collection.html.

There are many good books on STL (the Standard Template Library for C++, which has a rich collections library). It's difficult, however, to find good online information on STL maps. One worthwhile entry point for STL info in general, and information on <map> routines in particular, is http://www.dinkumware.com/htm_stl/index.html. Silicon Graphics, Inc. also maintains a very detailed online STL reference: see http://www.sgi.com/Technology/STL/table_of_contents.html.But bear in mind that the SGI implementation of STL contains SGI-defined extensions as well as standard C++ templates.

There are (IMHO) really no good books, at this point, and precious few decent online tutorials, on hash techniques. For instructive online examples, you should search the Google search engine using keywords "hash" and "cryptography." (One paper that's worth a careful look is http://www.cs.cmu.edu/~dst/CP4break/, "The Breaking of CyberPatrol 4," by Eddy L. O. Jansson and Matthew Skala.)


Kas Thomas has been programming in C and assembly on the Mac platform since before Windows existed. By day, he is a Senior Technical Writer for Silverstream Software (a maker of app-server softwar. You can reach him in care of editorial@mactech.com.

 
AAPL
$99.76
Apple Inc.
+2.09
MSFT
$44.08
Microsoft Corpora
+0.45
GOOG
$520.84
Google Inc.
+9.67

MacTech Search:
Community Search:

Software Updates via MacUpdate

RestoreMeNot 2.0.3 - Disable window rest...
RestoreMeNot provides a simple way to disable the window restoration for individual applications so that you can fine-tune this behavior to suit your needs. Please note that RestoreMeNot is designed... Read more
Macgo Blu-ray Player 2.10.9.1750 - Blu-r...
Macgo Mac Blu-ray Player can bring you the most unforgettable Blu-ray experience on your Mac. Overview Macgo Mac Blu-ray Player can satisfy just about every need you could possibly have in a Blu-ray... Read more
Apple iOS 8.1 - The latest version of Ap...
The latest version of iOS can be downloaded through iTunes. Apple iOS 8 comes with big updates to apps you use every day, like Messages and Photos. A whole new way to share content with your family.... Read more
TechTool Pro 7.0.5 - Hard drive and syst...
TechTool Pro is now 7, and this is the most advanced version of the acclaimed Macintosh troubleshooting utility created in its 20-year history. Micromat has redeveloped TechTool Pro 7 to be fully 64... Read more
PDFKey Pro 4.0.2 - Edit and print passwo...
PDFKey Pro can unlock PDF documents protected for printing and copying when you've forgotten your password. It can now also protect your PDF files with a password to prevent unauthorized access and/... Read more
Yasu 2.9.1 - System maintenance app; per...
Yasu was originally created with System Administrators who service large groups of workstations in mind, Yasu (Yet Another System Utility) was made to do a specific group of maintenance tasks... Read more
Hazel 3.3 - Create rules for organizing...
Hazel is your personal housekeeper, organizing and cleaning folders based on rules you define. Hazel can also manage your trash and uninstall your applications. Organize your files using a... Read more
Autopano Giga 3.7 - Stitch multiple imag...
Autopano Giga allows you to stitch 2, 20, or 2,000 images. Version 3.0 integrates impressive new features that will definitely make you adopt Autopano Pro or Autopano Giga: Choose between 9... Read more
MenuMeters 1.8 - CPU, memory, disk, and...
MenuMeters is a set of CPU, memory, disk, and network monitoring tools for Mac OS X. Although there are numerous other programs which do the same thing, none had quite the feature set I was looking... Read more
Coda 2.5 - One-window Web development su...
Coda is a powerful Web editor that puts everything in one place. An editor. Terminal. CSS. Files. With Coda 2, we went beyond expectations. With loads of new, much-requested features, a few... Read more

Latest Forum Discussions

See All

This Week at 148Apps: October 13-17, 201...
Expert App Reviewers   So little time and so very many apps. What’s a poor iPhone/iPad lover to do? Fortunately, 148Apps is here to give you the rundown on the latest and greatest releases. And we even have a tremendous back catalog of reviews; just... | Read more »
Angry Birds Transformers Review
Angry Birds Transformers Review By Jennifer Allen on October 20th, 2014 Our Rating: :: TRANSFORMED BIRDSUniversal App - Designed for iPhone and iPad Transformed in a way you wouldn’t expect, Angry Birds Transformers is a quite... | Read more »
GAMEVIL Announces the Upcoming Launch of...
GAMEVIL Announces the Upcoming Launch of Mark of the Dragon Posted by Jessica Fisher on October 20th, 2014 [ permalink ] Mark of the Dragon, by GAMEVIL, put | Read more »
Interview With the Angry Birds Transform...
Angry Birds Transformers recently transformed and rolled out worldwide. This run-and-gun title is a hit with young Transformers fans, but the ample references to classic Transformers fandom has also earned it a place in the hearts of long-time... | Read more »
Find Free Food on Campus with Ypay
Find Free Food on Campus with Ypay Posted by Jessica Fisher on October 20th, 2014 [ permalink ] iPhone App - Designed for the iPhone, compatible with the iPad | Read more »
Strung Along Review
Strung Along Review By Jordan Minor on October 20th, 2014 Our Rating: :: GOT NO STRINGSUniversal App - Designed for iPhone and iPad A cool gimmick and a great art style keep Strung Along from completely falling apart.   | Read more »
P2P file transferring app Send Anywhere...
File sharing services like Dropbox have security issues. Email attachments can be problematic when it comes to sharing large files. USB dongles don’t fit into your phone. Send Anywhere, a peer-to-peer file transferring application, solves all of... | Read more »
Zero Age Review
Zero Age Review By Jordan Minor on October 20th, 2014 Our Rating: :: MORE THAN ZEROiPad Only App - Designed for the iPad With its mind-bending puzzles and spellbinding visuals, Zero Age has it all.   | Read more »
Hay Ewe Review
Hay Ewe Review By Campbell Bird on October 20th, 2014 Our Rating: :: SAVE YOUR SHEEPLEUniversal App - Designed for iPhone and iPad Pave the way for your flock in this line drawing puzzle game from the creators of Worms.   | Read more »
My Very Hungry Caterpillar (Education)
My Very Hungry Caterpillar 1.0.0 Device: iOS Universal Category: Education Price: $3.99, Version: 1.0.0 (iTunes) Description: Care for your very own Very Hungry Caterpillar! My Very Hungry Caterpillar will captivate you as he crawls... | Read more »

Price Scanner via MacPrices.net

2013 15-inch 2.0GHz Retina MacBook Pro availa...
B&H Photo has leftover previous-generation 15″ 2.0GHz Retina MacBook Pros now available for $1599 including free shipping plus NY sales tax only. Their price is $400 off original MSRP. B&H... Read more
Updated iPad Prices
We’ve updated our iPad Air Price Tracker and our iPad mini Price Tracker with the latest information on prices and availability from Apple and other resellers, including the new iPad Air 2 and the... Read more
Apple Pay Available to Millions of Visa Cardh...
Visa Inc. brings secure, convenient payments to iPad Air 2 and iPad mini 3as well as iPhone 6 and 6 Plus. Starting October 20th, eligible Visa cardholders in the U.S. will be able to use Apple Pay,... Read more
Textkraft Pocket – the missing TextEdit for i...
infovole GmbH has announced the release and immediate availability of Textkraft Pocket 1.0, a professional text editor and note taking app for Apple’s iPhone. In March 2014 rumors were all about... Read more
C Spire to offer iPad Air 2 and iPad mini 3,...
C Spire on Friday announced that it will offer iPad Air 2 and iPad mini 3, both with Wi-Fi + Cellular, on its 4G+ LTE network in the coming weeks. C Spire will offer the new iPads with a range of... Read more
Belkin Announces Full Line of Keyboards and C...
Belkin International has unveiled a new lineup of keyboard cases and accessories for Apple’s newest iPads, featuring three QODE keyboards and a collection of thin, lightweight folios for both the... Read more
Verizon offers new iPad Air 2 preorders for $...
Verizon Wireless is accepting preorders for the new iPad Air 2, cellular models, for $100 off MSRP with a 2-year service agreement: - 16GB iPad Air 2 WiFi + Cellular: $529.99 - 64GB iPad Air 2 WiFi... Read more
Price drops on refurbished Mac minis, now ava...
The Apple Store has dropped prices on Apple Certified Refurbished previous-generation Mac minis, with models now available starting at $419. Apple’s one-year warranty is included with each mini, and... Read more
Apple refurbished 2014 MacBook Airs available...
The Apple Store has Apple Certified Refurbished 2014 MacBook Airs available for up to $180 off the cost of new models. An Apple one-year warranty is included with each MacBook, and shipping is free.... Read more
Refurbished 2013 MacBook Pros available for u...
The Apple Store has Apple Certified Refurbished 13″ and 15″ MacBook Pros available starting at $929. Apple’s one-year warranty is standard, and shipping is free: - 13″ 2.5GHz MacBook Pros (4GB RAM/... Read more

Jobs Board

Project Manager / Business Analyst, WW *Appl...
…a senior project manager / business analyst to work within our Worldwide Apple Fulfillment Operations and the Business Process Re-engineering team. This role will work Read more
*Apple* Retail - Multiple Positions (US) - A...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Position Opening at *Apple* - Apple (United...
…customers purchase our products, you're the one who helps them get more out of their new Apple technology. Your day in the Apple Store is filled with a range of Read more
Position Opening at *Apple* - Apple (United...
**Job Summary** At the Apple Store, you connect business professionals and entrepreneurs with the tools they need in order to put Apple solutions to work in their Read more
Position Opening at *Apple* - Apple (United...
**Job Summary** The Apple Store is a retail environment like no other - uniquely focused on delivering amazing customer experiences. As an Expert, you introduce people Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.